Bug 20582: Fix a cache issue in Koha::App::{Opac,Intranet}
[koha.git] / C4 / Items.pm
blob9da7f61fc657fee9b40ffe6b7d283a1e37a064da
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);
339 Marks an item as being transferred from one branch to another and records the trigger.
341 =cut
343 sub ModItemTransfer {
344 my ( $itemnumber, $frombranch, $tobranch, $trigger ) = @_;
346 my $dbh = C4::Context->dbh;
347 my $item = Koha::Items->find( $itemnumber );
349 # Remove the 'shelving cart' location status if it is being used.
350 CartToShelf( $itemnumber ) if $item->location && $item->location eq 'CART' && ( !$item->permanent_location || $item->permanent_location ne 'CART' );
352 $dbh->do("UPDATE branchtransfers SET datearrived = NOW(), comments = ? WHERE itemnumber = ? AND datearrived IS NULL", undef, "Canceled, new transfer from $frombranch to $tobranch created", $itemnumber);
354 #new entry in branchtransfers....
355 my $sth = $dbh->prepare(
356 "INSERT INTO branchtransfers (itemnumber, frombranch, datesent, tobranch, reason)
357 VALUES (?, ?, NOW(), ?, ?)");
358 $sth->execute($itemnumber, $frombranch, $tobranch, $trigger);
360 # FIXME we are fetching the item twice in the 2 next statements!
361 Koha::Items->find($itemnumber)->holdingbranch($frombranch)->store({ log_action => 0 });
362 ModDateLastSeen($itemnumber);
363 return;
366 =head2 ModDateLastSeen
368 ModDateLastSeen( $itemnumber, $leave_item_lost );
370 Mark item as seen. Is called when an item is issued, returned or manually marked during inventory/stocktaking.
371 C<$itemnumber> is the item number
372 C<$leave_item_lost> determines if a lost item will be found or remain lost
374 =cut
376 sub ModDateLastSeen {
377 my ( $itemnumber, $leave_item_lost ) = @_;
379 my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
381 my $item = Koha::Items->find($itemnumber);
382 $item->datelastseen($today);
383 $item->itemlost(0) unless $leave_item_lost;
384 $item->store({ log_action => 0 });
387 =head2 CheckItemPreSave
389 my $item_ref = TransformMarcToKoha($marc, 'items');
390 # do stuff
391 my %errors = CheckItemPreSave($item_ref);
392 if (exists $errors{'duplicate_barcode'}) {
393 print "item has duplicate barcode: ", $errors{'duplicate_barcode'}, "\n";
394 } elsif (exists $errors{'invalid_homebranch'}) {
395 print "item has invalid home branch: ", $errors{'invalid_homebranch'}, "\n";
396 } elsif (exists $errors{'invalid_holdingbranch'}) {
397 print "item has invalid holding branch: ", $errors{'invalid_holdingbranch'}, "\n";
398 } else {
399 print "item is OK";
402 Given a hashref containing item fields, determine if it can be
403 inserted or updated in the database. Specifically, checks for
404 database integrity issues, and returns a hash containing any
405 of the following keys, if applicable.
407 =over 2
409 =item duplicate_barcode
411 Barcode, if it duplicates one already found in the database.
413 =item invalid_homebranch
415 Home branch, if not defined in branches table.
417 =item invalid_holdingbranch
419 Holding branch, if not defined in branches table.
421 =back
423 This function does NOT implement any policy-related checks,
424 e.g., whether current operator is allowed to save an
425 item that has a given branch code.
427 =cut
429 sub CheckItemPreSave {
430 my $item_ref = shift;
432 my %errors = ();
434 # check for duplicate barcode
435 if (exists $item_ref->{'barcode'} and defined $item_ref->{'barcode'}) {
436 my $existing_item= Koha::Items->find({barcode => $item_ref->{'barcode'}});
437 if ($existing_item) {
438 if (!exists $item_ref->{'itemnumber'} # new item
439 or $item_ref->{'itemnumber'} != $existing_item->itemnumber) { # existing item
440 $errors{'duplicate_barcode'} = $item_ref->{'barcode'};
445 # check for valid home branch
446 if (exists $item_ref->{'homebranch'} and defined $item_ref->{'homebranch'}) {
447 my $home_library = Koha::Libraries->find( $item_ref->{homebranch} );
448 unless (defined $home_library) {
449 $errors{'invalid_homebranch'} = $item_ref->{'homebranch'};
453 # check for valid holding branch
454 if (exists $item_ref->{'holdingbranch'} and defined $item_ref->{'holdingbranch'}) {
455 my $holding_library = Koha::Libraries->find( $item_ref->{holdingbranch} );
456 unless (defined $holding_library) {
457 $errors{'invalid_holdingbranch'} = $item_ref->{'holdingbranch'};
461 return %errors;
465 =head1 EXPORTED SPECIAL ACCESSOR FUNCTIONS
467 The following functions provide various ways of
468 getting an item record, a set of item records, or
469 lists of authorized values for certain item fields.
471 =cut
473 =head2 GetItemsForInventory
475 ($itemlist, $iTotalRecords) = GetItemsForInventory( {
476 minlocation => $minlocation,
477 maxlocation => $maxlocation,
478 location => $location,
479 itemtype => $itemtype,
480 ignoreissued => $ignoreissued,
481 datelastseen => $datelastseen,
482 branchcode => $branchcode,
483 branch => $branch,
484 offset => $offset,
485 size => $size,
486 statushash => $statushash,
487 } );
489 Retrieve a list of title/authors/barcode/callnumber, for biblio inventory.
491 The sub returns a reference to a list of hashes, each containing
492 itemnumber, author, title, barcode, item callnumber, and date last
493 seen. It is ordered by callnumber then title.
495 The required minlocation & maxlocation parameters are used to specify a range of item callnumbers
496 the datelastseen can be used to specify that you want to see items not seen since a past date only.
497 offset & size can be used to retrieve only a part of the whole listing (defaut behaviour)
498 $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.
500 $iTotalRecords is the number of rows that would have been returned without the $offset, $size limit clause
502 =cut
504 sub GetItemsForInventory {
505 my ( $parameters ) = @_;
506 my $minlocation = $parameters->{'minlocation'} // '';
507 my $maxlocation = $parameters->{'maxlocation'} // '';
508 my $class_source = $parameters->{'class_source'} // C4::Context->preference('DefaultClassificationSource');
509 my $location = $parameters->{'location'} // '';
510 my $itemtype = $parameters->{'itemtype'} // '';
511 my $ignoreissued = $parameters->{'ignoreissued'} // '';
512 my $datelastseen = $parameters->{'datelastseen'} // '';
513 my $branchcode = $parameters->{'branchcode'} // '';
514 my $branch = $parameters->{'branch'} // '';
515 my $offset = $parameters->{'offset'} // '';
516 my $size = $parameters->{'size'} // '';
517 my $statushash = $parameters->{'statushash'} // '';
518 my $ignore_waiting_holds = $parameters->{'ignore_waiting_holds'} // '';
520 my $dbh = C4::Context->dbh;
521 my ( @bind_params, @where_strings );
523 my $min_cnsort = GetClassSort($class_source,undef,$minlocation);
524 my $max_cnsort = GetClassSort($class_source,undef,$maxlocation);
526 my $select_columns = q{
527 SELECT DISTINCT(items.itemnumber), barcode, itemcallnumber, title, author, biblio.biblionumber, biblio.frameworkcode, datelastseen, homebranch, location, notforloan, damaged, itemlost, withdrawn, stocknumber, items.cn_sort
529 my $select_count = q{SELECT COUNT(DISTINCT(items.itemnumber))};
530 my $query = q{
531 FROM items
532 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
533 LEFT JOIN biblioitems on items.biblionumber = biblioitems.biblionumber
535 if ($statushash){
536 for my $authvfield (keys %$statushash){
537 if ( scalar @{$statushash->{$authvfield}} > 0 ){
538 my $joinedvals = join ',', @{$statushash->{$authvfield}};
539 push @where_strings, "$authvfield in (" . $joinedvals . ")";
544 if ($minlocation) {
545 push @where_strings, 'items.cn_sort >= ?';
546 push @bind_params, $min_cnsort;
549 if ($maxlocation) {
550 push @where_strings, 'items.cn_sort <= ?';
551 push @bind_params, $max_cnsort;
554 if ($datelastseen) {
555 $datelastseen = output_pref({ str => $datelastseen, dateformat => 'iso', dateonly => 1 });
556 push @where_strings, '(datelastseen < ? OR datelastseen IS NULL)';
557 push @bind_params, $datelastseen;
560 if ( $location ) {
561 push @where_strings, 'items.location = ?';
562 push @bind_params, $location;
565 if ( $branchcode ) {
566 if($branch eq "homebranch"){
567 push @where_strings, 'items.homebranch = ?';
568 }else{
569 push @where_strings, 'items.holdingbranch = ?';
571 push @bind_params, $branchcode;
574 if ( $itemtype ) {
575 push @where_strings, 'biblioitems.itemtype = ?';
576 push @bind_params, $itemtype;
579 if ( $ignoreissued) {
580 $query .= "LEFT JOIN issues ON items.itemnumber = issues.itemnumber ";
581 push @where_strings, 'issues.date_due IS NULL';
584 if ( $ignore_waiting_holds ) {
585 $query .= "LEFT JOIN reserves ON items.itemnumber = reserves.itemnumber ";
586 push( @where_strings, q{(reserves.found != 'W' OR reserves.found IS NULL)} );
589 if ( @where_strings ) {
590 $query .= 'WHERE ';
591 $query .= join ' AND ', @where_strings;
593 my $count_query = $select_count . $query;
594 $query .= ' ORDER BY items.cn_sort, itemcallnumber, title';
595 $query .= " LIMIT $offset, $size" if ($offset and $size);
596 $query = $select_columns . $query;
597 my $sth = $dbh->prepare($query);
598 $sth->execute( @bind_params );
600 my @results = ();
601 my $tmpresults = $sth->fetchall_arrayref({});
602 $sth = $dbh->prepare( $count_query );
603 $sth->execute( @bind_params );
604 my ($iTotalRecords) = $sth->fetchrow_array();
606 my @avs = Koha::AuthorisedValues->search(
607 { 'marc_subfield_structures.kohafield' => { '>' => '' },
608 'me.authorised_value' => { '>' => '' },
610 { join => { category => 'marc_subfield_structures' },
611 distinct => ['marc_subfield_structures.kohafield, me.category, frameworkcode, me.authorised_value'],
612 '+select' => [ 'marc_subfield_structures.kohafield', 'marc_subfield_structures.frameworkcode', 'me.authorised_value', 'me.lib' ],
613 '+as' => [ 'kohafield', 'frameworkcode', 'authorised_value', 'lib' ],
617 my $avmapping = { map { $_->get_column('kohafield') . ',' . $_->get_column('frameworkcode') . ',' . $_->get_column('authorised_value') => $_->get_column('lib') } @avs };
619 foreach my $row (@$tmpresults) {
621 # Auth values
622 foreach (keys %$row) {
623 if (
624 defined(
625 $avmapping->{ "items.$_," . $row->{'frameworkcode'} . "," . ( $row->{$_} // q{} ) }
628 $row->{$_} = $avmapping->{"items.$_,".$row->{'frameworkcode'}.",".$row->{$_}};
631 push @results, $row;
634 return (\@results, $iTotalRecords);
637 =head2 GetItemsInfo
639 @results = GetItemsInfo($biblionumber);
641 Returns information about items with the given biblionumber.
643 C<GetItemsInfo> returns a list of references-to-hash. Each element
644 contains a number of keys. Most of them are attributes from the
645 C<biblio>, C<biblioitems>, C<items>, and C<itemtypes> tables in the
646 Koha database. Other keys include:
648 =over 2
650 =item C<$data-E<gt>{branchname}>
652 The name (not the code) of the branch to which the book belongs.
654 =item C<$data-E<gt>{datelastseen}>
656 This is simply C<items.datelastseen>, except that while the date is
657 stored in YYYY-MM-DD format in the database, here it is converted to
658 DD/MM/YYYY format. A NULL date is returned as C<//>.
660 =item C<$data-E<gt>{datedue}>
662 =item C<$data-E<gt>{class}>
664 This is the concatenation of C<biblioitems.classification>, the book's
665 Dewey code, and C<biblioitems.subclass>.
667 =item C<$data-E<gt>{ocount}>
669 I think this is the number of copies of the book available.
671 =item C<$data-E<gt>{order}>
673 If this is set, it is set to C<One Order>.
675 =back
677 =cut
679 sub GetItemsInfo {
680 my ( $biblionumber ) = @_;
681 my $dbh = C4::Context->dbh;
682 require C4::Languages;
683 my $language = C4::Languages::getlanguage();
684 my $query = "
685 SELECT items.*,
686 biblio.*,
687 biblioitems.volume,
688 biblioitems.number,
689 biblioitems.itemtype,
690 biblioitems.isbn,
691 biblioitems.issn,
692 biblioitems.publicationyear,
693 biblioitems.publishercode,
694 biblioitems.volumedate,
695 biblioitems.volumedesc,
696 biblioitems.lccn,
697 biblioitems.url,
698 items.notforloan as itemnotforloan,
699 issues.borrowernumber,
700 issues.date_due as datedue,
701 issues.onsite_checkout,
702 borrowers.cardnumber,
703 borrowers.surname,
704 borrowers.firstname,
705 borrowers.branchcode as bcode,
706 serial.serialseq,
707 serial.publisheddate,
708 itemtypes.description,
709 COALESCE( localization.translation, itemtypes.description ) AS translated_description,
710 itemtypes.notforloan as notforloan_per_itemtype,
711 holding.branchurl,
712 holding.branchcode,
713 holding.branchname,
714 holding.opac_info as holding_branch_opac_info,
715 home.opac_info as home_branch_opac_info,
716 IF(tmp_holdsqueue.itemnumber,1,0) AS has_pending_hold
717 FROM items
718 LEFT JOIN branches AS holding ON items.holdingbranch = holding.branchcode
719 LEFT JOIN branches AS home ON items.homebranch=home.branchcode
720 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
721 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
722 LEFT JOIN issues USING (itemnumber)
723 LEFT JOIN borrowers USING (borrowernumber)
724 LEFT JOIN serialitems USING (itemnumber)
725 LEFT JOIN serial USING (serialid)
726 LEFT JOIN itemtypes ON itemtypes.itemtype = "
727 . (C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype');
728 $query .= q|
729 LEFT JOIN tmp_holdsqueue USING (itemnumber)
730 LEFT JOIN localization ON itemtypes.itemtype = localization.code
731 AND localization.entity = 'itemtypes'
732 AND localization.lang = ?
735 $query .= " WHERE items.biblionumber = ? ORDER BY home.branchname, items.enumchron, LPAD( items.copynumber, 8, '0' ), items.dateaccessioned DESC" ;
736 my $sth = $dbh->prepare($query);
737 $sth->execute($language, $biblionumber);
738 my $i = 0;
739 my @results;
740 my $serial;
742 my $userenv = C4::Context->userenv;
743 my $want_not_same_branch = C4::Context->preference("IndependentBranches") && !C4::Context->IsSuperLibrarian();
744 while ( my $data = $sth->fetchrow_hashref ) {
745 if ( $data->{borrowernumber} && $want_not_same_branch) {
746 $data->{'NOTSAMEBRANCH'} = $data->{'bcode'} ne $userenv->{branch};
749 $serial ||= $data->{'serial'};
751 my $descriptions;
752 # get notforloan complete status if applicable
753 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.notforloan', authorised_value => $data->{itemnotforloan} });
754 $data->{notforloanvalue} = $descriptions->{lib} // '';
755 $data->{notforloanvalueopac} = $descriptions->{opac_description} // '';
757 # get restricted status and description if applicable
758 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.restricted', authorised_value => $data->{restricted} });
759 $data->{restrictedvalue} = $descriptions->{lib} // '';
760 $data->{restrictedvalueopac} = $descriptions->{opac_description} // '';
762 # my stack procedures
763 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.stack', authorised_value => $data->{stack} });
764 $data->{stack} = $descriptions->{lib} // '';
766 # Find the last 3 people who borrowed this item.
767 my $sth2 = $dbh->prepare("SELECT * FROM old_issues,borrowers
768 WHERE itemnumber = ?
769 AND old_issues.borrowernumber = borrowers.borrowernumber
770 ORDER BY returndate DESC
771 LIMIT 3");
772 $sth2->execute($data->{'itemnumber'});
773 my $ii = 0;
774 while (my $data2 = $sth2->fetchrow_hashref()) {
775 $data->{"timestamp$ii"} = $data2->{'timestamp'} if $data2->{'timestamp'};
776 $data->{"card$ii"} = $data2->{'cardnumber'} if $data2->{'cardnumber'};
777 $data->{"borrower$ii"} = $data2->{'borrowernumber'} if $data2->{'borrowernumber'};
778 $ii++;
781 $results[$i] = $data;
782 $i++;
785 return $serial
786 ? sort { ($b->{'publisheddate'} || $b->{'enumchron'}) cmp ($a->{'publisheddate'} || $a->{'enumchron'}) } @results
787 : @results;
790 =head2 GetItemsLocationInfo
792 my @itemlocinfo = GetItemsLocationInfo($biblionumber);
794 Returns the branch names, shelving location and itemcallnumber for each item attached to the biblio in question
796 C<GetItemsInfo> returns a list of references-to-hash. Data returned:
798 =over 2
800 =item C<$data-E<gt>{homebranch}>
802 Branch Name of the item's homebranch
804 =item C<$data-E<gt>{holdingbranch}>
806 Branch Name of the item's holdingbranch
808 =item C<$data-E<gt>{location}>
810 Item's shelving location code
812 =item C<$data-E<gt>{location_intranet}>
814 The intranet description for the Shelving Location as set in authorised_values 'LOC'
816 =item C<$data-E<gt>{location_opac}>
818 The OPAC description for the Shelving Location as set in authorised_values 'LOC'. Falls back to intranet description if no OPAC
819 description is set.
821 =item C<$data-E<gt>{itemcallnumber}>
823 Item's itemcallnumber
825 =item C<$data-E<gt>{cn_sort}>
827 Item's call number normalized for sorting
829 =back
831 =cut
833 sub GetItemsLocationInfo {
834 my $biblionumber = shift;
835 my @results;
837 my $dbh = C4::Context->dbh;
838 my $query = "SELECT a.branchname as homebranch, b.branchname as holdingbranch,
839 location, itemcallnumber, cn_sort
840 FROM items, branches as a, branches as b
841 WHERE homebranch = a.branchcode AND holdingbranch = b.branchcode
842 AND biblionumber = ?
843 ORDER BY cn_sort ASC";
844 my $sth = $dbh->prepare($query);
845 $sth->execute($biblionumber);
847 while ( my $data = $sth->fetchrow_hashref ) {
848 my $av = Koha::AuthorisedValues->search({ category => 'LOC', authorised_value => $data->{location} });
849 $av = $av->count ? $av->next : undef;
850 $data->{location_intranet} = $av ? $av->lib : '';
851 $data->{location_opac} = $av ? $av->opac_description : '';
852 push @results, $data;
854 return @results;
857 =head2 GetHostItemsInfo
859 $hostiteminfo = GetHostItemsInfo($hostfield);
860 Returns the iteminfo for items linked to records via a host field
862 =cut
864 sub GetHostItemsInfo {
865 my ($record) = @_;
866 my @returnitemsInfo;
868 if( !C4::Context->preference('EasyAnalyticalRecords') ) {
869 return @returnitemsInfo;
872 my @fields;
873 if( C4::Context->preference('marcflavour') eq 'MARC21' ||
874 C4::Context->preference('marcflavour') eq 'NORMARC') {
875 @fields = $record->field('773');
876 } elsif( C4::Context->preference('marcflavour') eq 'UNIMARC') {
877 @fields = $record->field('461');
880 foreach my $hostfield ( @fields ) {
881 my $hostbiblionumber = $hostfield->subfield("0");
882 my $linkeditemnumber = $hostfield->subfield("9");
883 my @hostitemInfos = GetItemsInfo($hostbiblionumber);
884 foreach my $hostitemInfo (@hostitemInfos) {
885 if( $hostitemInfo->{itemnumber} eq $linkeditemnumber ) {
886 push @returnitemsInfo, $hostitemInfo;
887 last;
891 return @returnitemsInfo;
894 =head2 get_hostitemnumbers_of
896 my @itemnumbers_of = get_hostitemnumbers_of($biblionumber);
898 Given a biblionumber, return the list of corresponding itemnumbers that are linked to it via host fields
900 Return a reference on a hash where key is a biblionumber and values are
901 references on array of itemnumbers.
903 =cut
906 sub get_hostitemnumbers_of {
907 my ($biblionumber) = @_;
909 if( !C4::Context->preference('EasyAnalyticalRecords') ) {
910 return ();
913 my $marcrecord = C4::Biblio::GetMarcBiblio({ biblionumber => $biblionumber });
914 return unless $marcrecord;
916 my ( @returnhostitemnumbers, $tag, $biblio_s, $item_s );
918 my $marcflavor = C4::Context->preference('marcflavour');
919 if ( $marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC' ) {
920 $tag = '773';
921 $biblio_s = '0';
922 $item_s = '9';
924 elsif ( $marcflavor eq 'UNIMARC' ) {
925 $tag = '461';
926 $biblio_s = '0';
927 $item_s = '9';
930 foreach my $hostfield ( $marcrecord->field($tag) ) {
931 my $hostbiblionumber = $hostfield->subfield($biblio_s);
932 next unless $hostbiblionumber; # have tag, don't have $biblio_s subfield
933 my $linkeditemnumber = $hostfield->subfield($item_s);
934 if ( ! $linkeditemnumber ) {
935 warn "ERROR biblionumber $biblionumber has 773^0, but doesn't have 9";
936 next;
938 my $is_from_biblio = Koha::Items->search({ itemnumber => $linkeditemnumber, biblionumber => $hostbiblionumber });
939 push @returnhostitemnumbers, $linkeditemnumber
940 if $is_from_biblio;
943 return @returnhostitemnumbers;
946 =head2 GetHiddenItemnumbers
948 my @itemnumbers_to_hide = GetHiddenItemnumbers({ items => \@items, borcat => $category });
950 Given a list of items it checks which should be hidden from the OPAC given
951 the current configuration. Returns a list of itemnumbers corresponding to
952 those that should be hidden. Optionally takes a borcat parameter for certain borrower types
953 to be excluded
955 =cut
957 sub GetHiddenItemnumbers {
958 my $params = shift;
959 my $items = $params->{items};
960 if (my $exceptions = C4::Context->preference('OpacHiddenItemsExceptions') and $params->{'borcat'}){
961 foreach my $except (split(/\|/, $exceptions)){
962 if ($params->{'borcat'} eq $except){
963 return; # we don't hide anything for this borrower category
967 my @resultitems;
969 my $yaml = C4::Context->preference('OpacHiddenItems');
970 return () if (! $yaml =~ /\S/ );
971 $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
972 my $hidingrules;
973 eval {
974 $hidingrules = YAML::Load($yaml);
976 if ($@) {
977 warn "Unable to parse OpacHiddenItems syspref : $@";
978 return ();
980 my $dbh = C4::Context->dbh;
982 # For each item
983 foreach my $item (@$items) {
985 # We check each rule
986 foreach my $field (keys %$hidingrules) {
987 my $val;
988 if (exists $item->{$field}) {
989 $val = $item->{$field};
991 else {
992 my $query = "SELECT $field from items where itemnumber = ?";
993 $val = $dbh->selectrow_array($query, undef, $item->{'itemnumber'});
995 $val = '' unless defined $val;
997 # If the results matches the values in the yaml file
998 if (any { $val eq $_ } @{$hidingrules->{$field}}) {
1000 # We add the itemnumber to the list
1001 push @resultitems, $item->{'itemnumber'};
1003 # If at least one rule matched for an item, no need to test the others
1004 last;
1008 return @resultitems;
1011 =head1 LIMITED USE FUNCTIONS
1013 The following functions, while part of the public API,
1014 are not exported. This is generally because they are
1015 meant to be used by only one script for a specific
1016 purpose, and should not be used in any other context
1017 without careful thought.
1019 =cut
1021 =head2 GetMarcItem
1023 my $item_marc = GetMarcItem($biblionumber, $itemnumber);
1025 Returns MARC::Record of the item passed in parameter.
1026 This function is meant for use only in C<cataloguing/additem.pl>,
1027 where it is needed to support that script's MARC-like
1028 editor.
1030 =cut
1032 sub GetMarcItem {
1033 my ( $biblionumber, $itemnumber ) = @_;
1035 # GetMarcItem has been revised so that it does the following:
1036 # 1. Gets the item information from the items table.
1037 # 2. Converts it to a MARC field for storage in the bib record.
1039 # The previous behavior was:
1040 # 1. Get the bib record.
1041 # 2. Return the MARC tag corresponding to the item record.
1043 # The difference is that one treats the items row as authoritative,
1044 # while the other treats the MARC representation as authoritative
1045 # under certain circumstances.
1047 my $item = Koha::Items->find($itemnumber) or return;
1049 # Tack on 'items.' prefix to column names so that C4::Biblio::TransformKohaToMarc will work.
1050 # Also, don't emit a subfield if the underlying field is blank.
1052 return Item2Marc($item->unblessed, $biblionumber);
1055 sub Item2Marc {
1056 my ($itemrecord,$biblionumber)=@_;
1057 my $mungeditem = {
1058 map {
1059 defined($itemrecord->{$_}) && $itemrecord->{$_} ne '' ? ("items.$_" => $itemrecord->{$_}) : ()
1060 } keys %{ $itemrecord }
1062 my $framework = C4::Biblio::GetFrameworkCode( $biblionumber );
1063 my $itemmarc = C4::Biblio::TransformKohaToMarc( $mungeditem, { framework => $framework } );
1064 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField(
1065 "items.itemnumber", $framework,
1068 my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($mungeditem->{'items.more_subfields_xml'});
1069 if (defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1) {
1070 foreach my $field ($itemmarc->field($itemtag)){
1071 $field->add_subfields(@$unlinked_item_subfields);
1074 return $itemmarc;
1077 =head1 PRIVATE FUNCTIONS AND VARIABLES
1079 The following functions are not meant to be called
1080 directly, but are documented in order to explain
1081 the inner workings of C<C4::Items>.
1083 =cut
1085 =head2 MoveItemFromBiblio
1087 MoveItemFromBiblio($itenumber, $frombiblio, $tobiblio);
1089 Moves an item from a biblio to another
1091 Returns undef if the move failed or the biblionumber of the destination record otherwise
1093 =cut
1095 sub MoveItemFromBiblio {
1096 my ($itemnumber, $frombiblio, $tobiblio) = @_;
1097 my $dbh = C4::Context->dbh;
1098 my ( $tobiblioitem ) = $dbh->selectrow_array(q|
1099 SELECT biblioitemnumber
1100 FROM biblioitems
1101 WHERE biblionumber = ?
1102 |, undef, $tobiblio );
1103 my $return = $dbh->do(q|
1104 UPDATE items
1105 SET biblioitemnumber = ?,
1106 biblionumber = ?
1107 WHERE itemnumber = ?
1108 AND biblionumber = ?
1109 |, undef, $tobiblioitem, $tobiblio, $itemnumber, $frombiblio );
1110 if ($return == 1) {
1111 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
1112 $indexer->index_records( $tobiblio, "specialUpdate", "biblioserver" );
1113 $indexer->index_records( $frombiblio, "specialUpdate", "biblioserver" );
1114 # Checking if the item we want to move is in an order
1115 require C4::Acquisition;
1116 my $order = C4::Acquisition::GetOrderFromItemnumber($itemnumber);
1117 if ($order) {
1118 # Replacing the biblionumber within the order if necessary
1119 $order->{'biblionumber'} = $tobiblio;
1120 C4::Acquisition::ModOrder($order);
1123 # Update reserves, hold_fill_targets, tmp_holdsqueue and linktracker tables
1124 for my $table_name ( qw( reserves hold_fill_targets tmp_holdsqueue linktracker ) ) {
1125 $dbh->do( qq|
1126 UPDATE $table_name
1127 SET biblionumber = ?
1128 WHERE itemnumber = ?
1129 |, undef, $tobiblio, $itemnumber );
1131 return $tobiblio;
1133 return;
1136 =head2 _marc_from_item_hash
1138 my $item_marc = _marc_from_item_hash($item, $frameworkcode[, $unlinked_item_subfields]);
1140 Given an item hash representing a complete item record,
1141 create a C<MARC::Record> object containing an embedded
1142 tag representing that item.
1144 The third, optional parameter C<$unlinked_item_subfields> is
1145 an arrayref of subfields (not mapped to C<items> fields per the
1146 framework) to be added to the MARC representation
1147 of the item.
1149 =cut
1151 sub _marc_from_item_hash {
1152 my $item = shift;
1153 my $frameworkcode = shift;
1154 my $unlinked_item_subfields;
1155 if (@_) {
1156 $unlinked_item_subfields = shift;
1159 # Tack on 'items.' prefix to column names so lookup from MARC frameworks will work
1160 # Also, don't emit a subfield if the underlying field is blank.
1161 my $mungeditem = { map { (defined($item->{$_}) and $item->{$_} ne '') ?
1162 (/^items\./ ? ($_ => $item->{$_}) : ("items.$_" => $item->{$_}))
1163 : () } keys %{ $item } };
1165 my $item_marc = MARC::Record->new();
1166 foreach my $item_field ( keys %{$mungeditem} ) {
1167 my ( $tag, $subfield ) = C4::Biblio::GetMarcFromKohaField( $item_field );
1168 next unless defined $tag and defined $subfield; # skip if not mapped to MARC field
1169 my @values = split(/\s?\|\s?/, $mungeditem->{$item_field}, -1);
1170 foreach my $value (@values){
1171 if ( my $field = $item_marc->field($tag) ) {
1172 $field->add_subfields( $subfield => $value );
1173 } else {
1174 my $add_subfields = [];
1175 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
1176 $add_subfields = $unlinked_item_subfields;
1178 $item_marc->add_fields( $tag, " ", " ", $subfield => $value, @$add_subfields );
1183 return $item_marc;
1186 =head2 _repack_item_errors
1188 Add an error message hash generated by C<CheckItemPreSave>
1189 to a list of errors.
1191 =cut
1193 sub _repack_item_errors {
1194 my $item_sequence_num = shift;
1195 my $item_ref = shift;
1196 my $error_ref = shift;
1198 my @repacked_errors = ();
1200 foreach my $error_code (sort keys %{ $error_ref }) {
1201 my $repacked_error = {};
1202 $repacked_error->{'item_sequence'} = $item_sequence_num;
1203 $repacked_error->{'item_barcode'} = exists($item_ref->{'barcode'}) ? $item_ref->{'barcode'} : '';
1204 $repacked_error->{'error_code'} = $error_code;
1205 $repacked_error->{'error_information'} = $error_ref->{$error_code};
1206 push @repacked_errors, $repacked_error;
1209 return @repacked_errors;
1212 =head2 _get_unlinked_item_subfields
1214 my $unlinked_item_subfields = _get_unlinked_item_subfields($original_item_marc, $frameworkcode);
1216 =cut
1218 sub _get_unlinked_item_subfields {
1219 my $original_item_marc = shift;
1220 my $frameworkcode = shift;
1222 my $marcstructure = C4::Biblio::GetMarcStructure(1, $frameworkcode, { unsafe => 1 });
1224 # assume that this record has only one field, and that that
1225 # field contains only the item information
1226 my $subfields = [];
1227 my @fields = $original_item_marc->fields();
1228 if ($#fields > -1) {
1229 my $field = $fields[0];
1230 my $tag = $field->tag();
1231 foreach my $subfield ($field->subfields()) {
1232 if (defined $subfield->[1] and
1233 $subfield->[1] ne '' and
1234 !$marcstructure->{$tag}->{$subfield->[0]}->{'kohafield'}) {
1235 push @$subfields, $subfield->[0] => $subfield->[1];
1239 return $subfields;
1242 =head2 _get_unlinked_subfields_xml
1244 my $unlinked_subfields_xml = _get_unlinked_subfields_xml($unlinked_item_subfields);
1246 =cut
1248 sub _get_unlinked_subfields_xml {
1249 my $unlinked_item_subfields = shift;
1251 my $xml;
1252 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
1253 my $marc = MARC::Record->new();
1254 # use of tag 999 is arbitrary, and doesn't need to match the item tag
1255 # used in the framework
1256 $marc->append_fields(MARC::Field->new('999', ' ', ' ', @$unlinked_item_subfields));
1257 $marc->encoding("UTF-8");
1258 $xml = $marc->as_xml("USMARC");
1261 return $xml;
1264 =head2 _parse_unlinked_item_subfields_from_xml
1266 my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($whole_item->{'more_subfields_xml'}):
1268 =cut
1270 sub _parse_unlinked_item_subfields_from_xml {
1271 my $xml = shift;
1272 require C4::Charset;
1273 return unless defined $xml and $xml ne "";
1274 my $marc = MARC::Record->new_from_xml(C4::Charset::StripNonXmlChars($xml),'UTF-8');
1275 my $unlinked_subfields = [];
1276 my @fields = $marc->fields();
1277 if ($#fields > -1) {
1278 foreach my $subfield ($fields[0]->subfields()) {
1279 push @$unlinked_subfields, $subfield->[0] => $subfield->[1];
1282 return $unlinked_subfields;
1285 =head2 GetAnalyticsCount
1287 $count= &GetAnalyticsCount($itemnumber)
1289 counts Usage of itemnumber in Analytical bibliorecords.
1291 =cut
1293 sub GetAnalyticsCount {
1294 my ($itemnumber) = @_;
1296 ### ZOOM search here
1297 my $query;
1298 $query= "hi=".$itemnumber;
1299 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
1300 my ($err,$res,$result) = $searcher->simple_search_compat($query,0,10);
1301 return ($result);
1304 sub _SearchItems_build_where_fragment {
1305 my ($filter) = @_;
1307 my $dbh = C4::Context->dbh;
1309 my $where_fragment;
1310 if (exists($filter->{conjunction})) {
1311 my (@where_strs, @where_args);
1312 foreach my $f (@{ $filter->{filters} }) {
1313 my $fragment = _SearchItems_build_where_fragment($f);
1314 if ($fragment) {
1315 push @where_strs, $fragment->{str};
1316 push @where_args, @{ $fragment->{args} };
1319 my $where_str = '';
1320 if (@where_strs) {
1321 $where_str = '(' . join (' ' . $filter->{conjunction} . ' ', @where_strs) . ')';
1322 $where_fragment = {
1323 str => $where_str,
1324 args => \@where_args,
1327 } else {
1328 my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1329 push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1330 push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1331 my @operators = qw(= != > < >= <= like);
1332 my $field = $filter->{field} // q{};
1333 if ( (0 < grep { $_ eq $field } @columns) or (substr($field, 0, 5) eq 'marc:') ) {
1334 my $op = $filter->{operator};
1335 my $query = $filter->{query};
1337 if (!$op or (0 == grep { $_ eq $op } @operators)) {
1338 $op = '='; # default operator
1341 my $column;
1342 if ($field =~ /^marc:(\d{3})(?:\$(\w))?$/) {
1343 my $marcfield = $1;
1344 my $marcsubfield = $2;
1345 my ($kohafield) = $dbh->selectrow_array(q|
1346 SELECT kohafield FROM marc_subfield_structure
1347 WHERE tagfield=? AND tagsubfield=? AND frameworkcode=''
1348 |, undef, $marcfield, $marcsubfield);
1350 if ($kohafield) {
1351 $column = $kohafield;
1352 } else {
1353 # MARC field is not linked to a DB field so we need to use
1354 # ExtractValue on marcxml from biblio_metadata or
1355 # items.more_subfields_xml, depending on the MARC field.
1356 my $xpath;
1357 my $sqlfield;
1358 my ($itemfield) = C4::Biblio::GetMarcFromKohaField('items.itemnumber');
1359 if ($marcfield eq $itemfield) {
1360 $sqlfield = 'more_subfields_xml';
1361 $xpath = '//record/datafield/subfield[@code="' . $marcsubfield . '"]';
1362 } else {
1363 $sqlfield = 'metadata'; # From biblio_metadata
1364 if ($marcfield < 10) {
1365 $xpath = "//record/controlfield[\@tag=\"$marcfield\"]";
1366 } else {
1367 $xpath = "//record/datafield[\@tag=\"$marcfield\"]/subfield[\@code=\"$marcsubfield\"]";
1370 $column = "ExtractValue($sqlfield, '$xpath')";
1372 } else {
1373 $column = $field;
1376 if (ref $query eq 'ARRAY') {
1377 if ($op eq '=') {
1378 $op = 'IN';
1379 } elsif ($op eq '!=') {
1380 $op = 'NOT IN';
1382 $where_fragment = {
1383 str => "$column $op (" . join (',', ('?') x @$query) . ")",
1384 args => $query,
1386 } else {
1387 $where_fragment = {
1388 str => "$column $op ?",
1389 args => [ $query ],
1395 return $where_fragment;
1398 =head2 SearchItems
1400 my ($items, $total) = SearchItems($filter, $params);
1402 Perform a search among items
1404 $filter is a reference to a hash which can be a filter, or a combination of filters.
1406 A filter has the following keys:
1408 =over 2
1410 =item * field: the name of a SQL column in table items
1412 =item * query: the value to search in this column
1414 =item * operator: comparison operator. Can be one of = != > < >= <= like
1416 =back
1418 A combination of filters hash the following keys:
1420 =over 2
1422 =item * conjunction: 'AND' or 'OR'
1424 =item * filters: array ref of filters
1426 =back
1428 $params is a reference to a hash that can contain the following parameters:
1430 =over 2
1432 =item * rows: Number of items to return. 0 returns everything (default: 0)
1434 =item * page: Page to return (return items from (page-1)*rows to (page*rows)-1)
1435 (default: 1)
1437 =item * sortby: A SQL column name in items table to sort on
1439 =item * sortorder: 'ASC' or 'DESC'
1441 =back
1443 =cut
1445 sub SearchItems {
1446 my ($filter, $params) = @_;
1448 $filter //= {};
1449 $params //= {};
1450 return unless ref $filter eq 'HASH';
1451 return unless ref $params eq 'HASH';
1453 # Default parameters
1454 $params->{rows} ||= 0;
1455 $params->{page} ||= 1;
1456 $params->{sortby} ||= 'itemnumber';
1457 $params->{sortorder} ||= 'ASC';
1459 my ($where_str, @where_args);
1460 my $where_fragment = _SearchItems_build_where_fragment($filter);
1461 if ($where_fragment) {
1462 $where_str = $where_fragment->{str};
1463 @where_args = @{ $where_fragment->{args} };
1466 my $dbh = C4::Context->dbh;
1467 my $query = q{
1468 SELECT SQL_CALC_FOUND_ROWS items.*
1469 FROM items
1470 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
1471 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
1472 LEFT JOIN biblio_metadata ON biblio_metadata.biblionumber = biblio.biblionumber
1473 WHERE 1
1475 if (defined $where_str and $where_str ne '') {
1476 $query .= qq{ AND $where_str };
1479 $query .= q{ AND biblio_metadata.format = 'marcxml' AND biblio_metadata.schema = ? };
1480 push @where_args, C4::Context->preference('marcflavour');
1482 my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1483 push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1484 push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1485 my $sortby = (0 < grep {$params->{sortby} eq $_} @columns)
1486 ? $params->{sortby} : 'itemnumber';
1487 my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
1488 $query .= qq{ ORDER BY $sortby $sortorder };
1490 my $rows = $params->{rows};
1491 my @limit_args;
1492 if ($rows > 0) {
1493 my $offset = $rows * ($params->{page}-1);
1494 $query .= qq { LIMIT ?, ? };
1495 push @limit_args, $offset, $rows;
1498 my $sth = $dbh->prepare($query);
1499 my $rv = $sth->execute(@where_args, @limit_args);
1501 return unless ($rv);
1502 my ($total_rows) = $dbh->selectrow_array(q{ SELECT FOUND_ROWS() });
1504 return ($sth->fetchall_arrayref({}), $total_rows);
1508 =head1 OTHER FUNCTIONS
1510 =head2 _find_value
1512 ($indicators, $value) = _find_value($tag, $subfield, $record,$encoding);
1514 Find the given $subfield in the given $tag in the given
1515 MARC::Record $record. If the subfield is found, returns
1516 the (indicators, value) pair; otherwise, (undef, undef) is
1517 returned.
1519 PROPOSITION :
1520 Such a function is used in addbiblio AND additem and serial-edit and maybe could be used in Authorities.
1521 I suggest we export it from this module.
1523 =cut
1525 sub _find_value {
1526 my ( $tagfield, $insubfield, $record, $encoding ) = @_;
1527 my @result;
1528 my $indicator;
1529 if ( $tagfield < 10 ) {
1530 if ( $record->field($tagfield) ) {
1531 push @result, $record->field($tagfield)->data();
1532 } else {
1533 push @result, "";
1535 } else {
1536 foreach my $field ( $record->field($tagfield) ) {
1537 my @subfields = $field->subfields();
1538 foreach my $subfield (@subfields) {
1539 if ( @$subfield[0] eq $insubfield ) {
1540 push @result, @$subfield[1];
1541 $indicator = $field->indicator(1) . $field->indicator(2);
1546 return ( $indicator, @result );
1550 =head2 PrepareItemrecordDisplay
1552 PrepareItemrecordDisplay($itemrecord,$bibnum,$itemumber,$frameworkcode);
1554 Returns a hash with all the fields for Display a given item data in a template
1556 The $frameworkcode returns the item for the given frameworkcode, ONLY if bibnum is not provided
1558 =cut
1560 sub PrepareItemrecordDisplay {
1562 my ( $bibnum, $itemnum, $defaultvalues, $frameworkcode ) = @_;
1564 my $dbh = C4::Context->dbh;
1565 $frameworkcode = C4::Biblio::GetFrameworkCode($bibnum) if $bibnum;
1566 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1568 # Note: $tagslib obtained from GetMarcStructure() in 'unsafe' mode is
1569 # a shared data structure. No plugin (including custom ones) should change
1570 # its contents. See also GetMarcStructure.
1571 my $tagslib = GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1573 # return nothing if we don't have found an existing framework.
1574 return q{} unless $tagslib;
1575 my $itemrecord;
1576 if ($itemnum) {
1577 $itemrecord = C4::Items::GetMarcItem( $bibnum, $itemnum );
1579 my @loop_data;
1581 my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
1582 my $query = qq{
1583 SELECT authorised_value,lib FROM authorised_values
1585 $query .= qq{
1586 LEFT JOIN authorised_values_branches ON ( id = av_id )
1587 } if $branch_limit;
1588 $query .= qq{
1589 WHERE category = ?
1591 $query .= qq{ AND ( branchcode = ? OR branchcode IS NULL )} if $branch_limit;
1592 $query .= qq{ ORDER BY lib};
1593 my $authorised_values_sth = $dbh->prepare( $query );
1594 foreach my $tag ( sort keys %{$tagslib} ) {
1595 if ( $tag ne '' ) {
1597 # loop through each subfield
1598 my $cntsubf;
1599 foreach my $subfield ( sort keys %{ $tagslib->{$tag} } ) {
1600 next if IsMarcStructureInternal($tagslib->{$tag}{$subfield});
1601 next unless ( $tagslib->{$tag}->{$subfield}->{'tab'} );
1602 next if ( $tagslib->{$tag}->{$subfield}->{'tab'} ne "10" );
1603 my %subfield_data;
1604 $subfield_data{tag} = $tag;
1605 $subfield_data{subfield} = $subfield;
1606 $subfield_data{countsubfield} = $cntsubf++;
1607 $subfield_data{kohafield} = $tagslib->{$tag}->{$subfield}->{'kohafield'};
1608 $subfield_data{id} = "tag_".$tag."_subfield_".$subfield."_".int(rand(1000000));
1610 # $subfield_data{marc_lib}=$tagslib->{$tag}->{$subfield}->{lib};
1611 $subfield_data{marc_lib} = $tagslib->{$tag}->{$subfield}->{lib};
1612 $subfield_data{mandatory} = $tagslib->{$tag}->{$subfield}->{mandatory};
1613 $subfield_data{repeatable} = $tagslib->{$tag}->{$subfield}->{repeatable};
1614 $subfield_data{hidden} = "display:none"
1615 if ( ( $tagslib->{$tag}->{$subfield}->{hidden} > 4 )
1616 || ( $tagslib->{$tag}->{$subfield}->{hidden} < -4 ) );
1617 my ( $x, $defaultvalue );
1618 if ($itemrecord) {
1619 ( $x, $defaultvalue ) = _find_value( $tag, $subfield, $itemrecord );
1621 $defaultvalue = $tagslib->{$tag}->{$subfield}->{defaultvalue} unless $defaultvalue;
1622 if ( !defined $defaultvalue ) {
1623 $defaultvalue = q||;
1624 } else {
1625 $defaultvalue =~ s/"/&quot;/g;
1626 # get today date & replace <<YYYY>>, <<MM>>, <<DD>> if provided in the default value
1627 my $today_dt = dt_from_string;
1628 my $year = $today_dt->strftime('%Y');
1629 my $shortyear = $today_dt->strftime('%y');
1630 my $month = $today_dt->strftime('%m');
1631 my $day = $today_dt->strftime('%d');
1632 $defaultvalue =~ s/<<YYYY>>/$year/g;
1633 $defaultvalue =~ s/<<YY>>/$shortyear/g;
1634 $defaultvalue =~ s/<<MM>>/$month/g;
1635 $defaultvalue =~ s/<<DD>>/$day/g;
1637 # And <<USER>> with surname (?)
1638 my $username =
1639 ( C4::Context->userenv
1640 ? C4::Context->userenv->{'surname'}
1641 : "superlibrarian" );
1642 $defaultvalue =~ s/<<USER>>/$username/g;
1645 my $maxlength = $tagslib->{$tag}->{$subfield}->{maxlength};
1647 # search for itemcallnumber if applicable
1648 if ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
1649 && C4::Context->preference('itemcallnumber') && $itemrecord) {
1650 foreach my $itemcn_pref (split(/,/,C4::Context->preference('itemcallnumber'))){
1651 my $CNtag = substr( $itemcn_pref, 0, 3 );
1652 next unless my $field = $itemrecord->field($CNtag);
1653 my $CNsubfields = substr( $itemcn_pref, 3 );
1654 $defaultvalue = $field->as_string( $CNsubfields, ' ');
1655 last if $defaultvalue;
1658 if ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
1659 && $defaultvalues
1660 && $defaultvalues->{'callnumber'} ) {
1661 if( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ){
1662 # if the item record exists, only use default value if the item has no callnumber
1663 $defaultvalue = $defaultvalues->{callnumber};
1664 } elsif ( !$itemrecord and $defaultvalues ) {
1665 # if the item record *doesn't* exists, always use the default value
1666 $defaultvalue = $defaultvalues->{callnumber};
1669 if ( ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.holdingbranch' || $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.homebranch' )
1670 && $defaultvalues
1671 && $defaultvalues->{'branchcode'} ) {
1672 if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
1673 $defaultvalue = $defaultvalues->{branchcode};
1676 if ( ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.location' )
1677 && $defaultvalues
1678 && $defaultvalues->{'location'} ) {
1680 if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
1681 # if the item record exists, only use default value if the item has no locationr
1682 $defaultvalue = $defaultvalues->{location};
1683 } elsif ( !$itemrecord and $defaultvalues ) {
1684 # if the item record *doesn't* exists, always use the default value
1685 $defaultvalue = $defaultvalues->{location};
1688 if ( $tagslib->{$tag}->{$subfield}->{authorised_value} ) {
1689 my @authorised_values;
1690 my %authorised_lib;
1692 # builds list, depending on authorised value...
1693 #---- branch
1694 if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "branches" ) {
1695 if ( ( C4::Context->preference("IndependentBranches") )
1696 && !C4::Context->IsSuperLibrarian() ) {
1697 my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches WHERE branchcode = ? ORDER BY branchname" );
1698 $sth->execute( C4::Context->userenv->{branch} );
1699 push @authorised_values, ""
1700 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1701 while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1702 push @authorised_values, $branchcode;
1703 $authorised_lib{$branchcode} = $branchname;
1705 } else {
1706 my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches ORDER BY branchname" );
1707 $sth->execute;
1708 push @authorised_values, ""
1709 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1710 while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1711 push @authorised_values, $branchcode;
1712 $authorised_lib{$branchcode} = $branchname;
1716 $defaultvalue = C4::Context->userenv ? C4::Context->userenv->{branch} : undef;
1717 if ( $defaultvalues and $defaultvalues->{branchcode} ) {
1718 $defaultvalue = $defaultvalues->{branchcode};
1721 #----- itemtypes
1722 } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "itemtypes" ) {
1723 my $itemtypes = Koha::ItemTypes->search_with_localization;
1724 push @authorised_values, "";
1725 while ( my $itemtype = $itemtypes->next ) {
1726 push @authorised_values, $itemtype->itemtype;
1727 $authorised_lib{$itemtype->itemtype} = $itemtype->translated_description;
1729 if ($defaultvalues && $defaultvalues->{'itemtype'}) {
1730 $defaultvalue = $defaultvalues->{'itemtype'};
1733 #---- class_sources
1734 } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "cn_source" ) {
1735 push @authorised_values, "";
1737 my $class_sources = GetClassSources();
1738 my $default_source = $defaultvalue || C4::Context->preference("DefaultClassificationSource");
1740 foreach my $class_source (sort keys %$class_sources) {
1741 next unless $class_sources->{$class_source}->{'used'} or
1742 ($class_source eq $default_source);
1743 push @authorised_values, $class_source;
1744 $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
1747 $defaultvalue = $default_source;
1749 #---- "true" authorised value
1750 } else {
1751 $authorised_values_sth->execute(
1752 $tagslib->{$tag}->{$subfield}->{authorised_value},
1753 $branch_limit ? $branch_limit : ()
1755 push @authorised_values, "";
1756 while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
1757 push @authorised_values, $value;
1758 $authorised_lib{$value} = $lib;
1761 $subfield_data{marc_value} = {
1762 type => 'select',
1763 values => \@authorised_values,
1764 default => $defaultvalue // q{},
1765 labels => \%authorised_lib,
1767 } elsif ( $tagslib->{$tag}->{$subfield}->{value_builder} ) {
1768 # it is a plugin
1769 require Koha::FrameworkPlugin;
1770 my $plugin = Koha::FrameworkPlugin->new({
1771 name => $tagslib->{$tag}->{$subfield}->{value_builder},
1772 item_style => 1,
1774 my $pars = { dbh => $dbh, record => undef, tagslib =>$tagslib, id => $subfield_data{id}, tabloop => undef };
1775 $plugin->build( $pars );
1776 if ( $itemrecord and my $field = $itemrecord->field($tag) ) {
1777 $defaultvalue = $field->subfield($subfield) || q{};
1779 if( !$plugin->errstr ) {
1780 #TODO Move html to template; see report 12176/13397
1781 my $tab= $plugin->noclick? '-1': '';
1782 my $class= $plugin->noclick? ' disabled': '';
1783 my $title= $plugin->noclick? 'No popup': 'Tag editor';
1784 $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;
1785 } else {
1786 warn $plugin->errstr;
1787 $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
1790 elsif ( $tag eq '' ) { # it's an hidden field
1791 $subfield_data{marc_value} = qq(<input type="hidden" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1793 elsif ( $tagslib->{$tag}->{$subfield}->{'hidden'} ) { # FIXME: shouldn't input type be "hidden" ?
1794 $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1796 elsif ( length($defaultvalue) > 100
1797 or (C4::Context->preference("marcflavour") eq "UNIMARC" and
1798 300 <= $tag && $tag < 400 && $subfield eq 'a' )
1799 or (C4::Context->preference("marcflavour") eq "MARC21" and
1800 500 <= $tag && $tag < 600 )
1802 # oversize field (textarea)
1803 $subfield_data{marc_value} = qq(<textarea id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength">$defaultvalue</textarea>\n");
1804 } else {
1805 $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1807 push( @loop_data, \%subfield_data );
1811 my $itemnumber;
1812 if ( $itemrecord && $itemrecord->field($itemtagfield) ) {
1813 $itemnumber = $itemrecord->subfield( $itemtagfield, $itemtagsubfield );
1815 return {
1816 'itemtagfield' => $itemtagfield,
1817 'itemtagsubfield' => $itemtagsubfield,
1818 'itemnumber' => $itemnumber,
1819 'iteminformation' => \@loop_data
1823 sub ToggleNewStatus {
1824 my ( $params ) = @_;
1825 my @rules = @{ $params->{rules} };
1826 my $report_only = $params->{report_only};
1828 my $dbh = C4::Context->dbh;
1829 my @errors;
1830 my @item_columns = map { "items.$_" } Koha::Items->columns;
1831 my @biblioitem_columns = map { "biblioitems.$_" } Koha::Biblioitems->columns;
1832 my $report;
1833 for my $rule ( @rules ) {
1834 my $age = $rule->{age};
1835 my $conditions = $rule->{conditions};
1836 my $substitutions = $rule->{substitutions};
1837 foreach ( @$substitutions ) {
1838 ( $_->{item_field} ) = ( $_->{field} =~ /items\.(.*)/ );
1840 my @params;
1842 my $query = q|
1843 SELECT items.*
1844 FROM items
1845 LEFT JOIN biblioitems ON biblioitems.biblionumber = items.biblionumber
1846 WHERE 1
1848 for my $condition ( @$conditions ) {
1849 if (
1850 grep { $_ eq $condition->{field} } @item_columns
1851 or grep { $_ eq $condition->{field} } @biblioitem_columns
1853 if ( $condition->{value} =~ /\|/ ) {
1854 my @values = split /\|/, $condition->{value};
1855 $query .= qq| AND $condition->{field} IN (|
1856 . join( ',', ('?') x scalar @values )
1857 . q|)|;
1858 push @params, @values;
1859 } else {
1860 $query .= qq| AND $condition->{field} = ?|;
1861 push @params, $condition->{value};
1865 if ( defined $age ) {
1866 $query .= q| AND TO_DAYS(NOW()) - TO_DAYS(dateaccessioned) >= ? |;
1867 push @params, $age;
1869 my $sth = $dbh->prepare($query);
1870 $sth->execute( @params );
1871 while ( my $values = $sth->fetchrow_hashref ) {
1872 my $biblionumber = $values->{biblionumber};
1873 my $itemnumber = $values->{itemnumber};
1874 my $item = Koha::Items->find($itemnumber);
1875 for my $substitution ( @$substitutions ) {
1876 my $field = $substitution->{item_field};
1877 my $value = $substitution->{value};
1878 next unless $substitution->{field};
1879 next if ( defined $values->{ $substitution->{item_field} } and $values->{ $substitution->{item_field} } eq $substitution->{value} );
1880 $item->$field($value);
1881 push @{ $report->{$itemnumber} }, $substitution;
1883 $item->store unless $report_only;
1887 return $report;