Bug 24027: Call ModZebra once after all items added/deleted in a batch
[koha.git] / C4 / Items.pm
blobc69e9d05756cb6da4847c459822ddaa63c2065e9
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::Search;
74 use Koha::Libraries;
76 =head1 NAME
78 C4::Items - item management functions
80 =head1 DESCRIPTION
82 This module contains an API for manipulating item
83 records in Koha, and is used by cataloguing, circulation,
84 acquisitions, and serials management.
86 # FIXME This POD is not up-to-date
87 A Koha item record is stored in two places: the
88 items table and embedded in a MARC tag in the XML
89 version of the associated bib record in C<biblioitems.marcxml>.
90 This is done to allow the item information to be readily
91 indexed (e.g., by Zebra), but means that each item
92 modification transaction must keep the items table
93 and the MARC XML in sync at all times.
95 The items table will be considered authoritative. In other
96 words, if there is ever a discrepancy between the items
97 table and the MARC XML, the items table should be considered
98 accurate.
100 =head1 HISTORICAL NOTE
102 Most of the functions in C<C4::Items> were originally in
103 the C<C4::Biblio> module.
105 =head1 CORE EXPORTED FUNCTIONS
107 The following functions are meant for use by users
108 of C<C4::Items>
110 =cut
112 =head2 CartToShelf
114 CartToShelf($itemnumber);
116 Set the current shelving location of the item record
117 to its stored permanent shelving location. This is
118 primarily used to indicate when an item whose current
119 location is a special processing ('PROC') or shelving cart
120 ('CART') location is back in the stacks.
122 =cut
124 sub CartToShelf {
125 my ( $itemnumber ) = @_;
127 unless ( $itemnumber ) {
128 croak "FAILED CartToShelf() - no itemnumber supplied";
131 my $item = Koha::Items->find($itemnumber);
132 if ( $item->location eq 'CART' ) {
133 $item->location($item->permanent_location)->store;
137 =head2 AddItemFromMarc
139 my ($biblionumber, $biblioitemnumber, $itemnumber)
140 = AddItemFromMarc($source_item_marc, $biblionumber[, $params]);
142 Given a MARC::Record object containing an embedded item
143 record and a biblionumber, create a new item record.
145 The final optional parameter, C<$params>, expected to contain
146 'skip_modzebra_update' key, which relayed down to Koha::Item/store,
147 there it prevents calling of ModZebra (and Elasticsearch update),
148 which takes most of the time in batch adds/deletes: ModZebra better
149 to be called later in C<additem.pl> after the whole loop.
151 $params:
152 skip_modzebra_update => true / false
153 =cut
155 sub AddItemFromMarc {
156 my $source_item_marc = shift;
157 my $biblionumber = shift;
158 my $params = @_ ? shift : {};
160 my $dbh = C4::Context->dbh;
162 # parse item hash from MARC
163 my $frameworkcode = C4::Biblio::GetFrameworkCode($biblionumber);
164 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
166 my $localitemmarc = MARC::Record->new;
167 $localitemmarc->append_fields( $source_item_marc->field($itemtag) );
169 my $item_values = C4::Biblio::TransformMarcToKoha( $localitemmarc, $frameworkcode, 'items' );
170 my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
171 $item_values->{more_subfields_xml} = _get_unlinked_subfields_xml($unlinked_item_subfields);
172 $item_values->{biblionumber} = $biblionumber;
173 $item_values->{cn_source} = delete $item_values->{'items.cn_source'}; # Because of C4::Biblio::_disambiguate
174 $item_values->{cn_sort} = delete $item_values->{'items.cn_sort'}; # Because of C4::Biblio::_disambiguate
175 my $item = Koha::Item->new( $item_values )->store({ skip_modzebra_update => $params->{skip_modzebra_update} });
176 return ( $item->biblionumber, $item->biblioitemnumber, $item->itemnumber );
179 =head2 AddItemBatchFromMarc
181 ($itemnumber_ref, $error_ref) = AddItemBatchFromMarc($record,
182 $biblionumber, $biblioitemnumber, $frameworkcode);
184 Efficiently create item records from a MARC biblio record with
185 embedded item fields. This routine is suitable for batch jobs.
187 This API assumes that the bib record has already been
188 saved to the C<biblio> and C<biblioitems> tables. It does
189 not expect that C<biblio_metadata.metadata> is populated, but it
190 will do so via a call to ModBibiloMarc.
192 The goal of this API is to have a similar effect to using AddBiblio
193 and AddItems in succession, but without inefficient repeated
194 parsing of the MARC XML bib record.
196 This function returns an arrayref of new itemsnumbers and an arrayref of item
197 errors encountered during the processing. Each entry in the errors
198 list is a hashref containing the following keys:
200 =over
202 =item item_sequence
204 Sequence number of original item tag in the MARC record.
206 =item item_barcode
208 Item barcode, provide to assist in the construction of
209 useful error messages.
211 =item error_code
213 Code representing the error condition. Can be 'duplicate_barcode',
214 'invalid_homebranch', or 'invalid_holdingbranch'.
216 =item error_information
218 Additional information appropriate to the error condition.
220 =back
222 =cut
224 sub AddItemBatchFromMarc {
225 my ($record, $biblionumber, $biblioitemnumber, $frameworkcode) = @_;
226 my $error;
227 my @itemnumbers = ();
228 my @errors = ();
229 my $dbh = C4::Context->dbh;
231 # We modify the record, so lets work on a clone so we don't change the
232 # original.
233 $record = $record->clone();
234 # loop through the item tags and start creating items
235 my @bad_item_fields = ();
236 my ($itemtag, $itemsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
237 my $item_sequence_num = 0;
238 ITEMFIELD: foreach my $item_field ($record->field($itemtag)) {
239 $item_sequence_num++;
240 # we take the item field and stick it into a new
241 # MARC record -- this is required so far because (FIXME)
242 # TransformMarcToKoha requires a MARC::Record, not a MARC::Field
243 # and there is no TransformMarcFieldToKoha
244 my $temp_item_marc = MARC::Record->new();
245 $temp_item_marc->append_fields($item_field);
247 # add biblionumber and biblioitemnumber
248 my $item = TransformMarcToKoha( $temp_item_marc, $frameworkcode, 'items' );
249 my $unlinked_item_subfields = _get_unlinked_item_subfields($temp_item_marc, $frameworkcode);
250 $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields);
251 $item->{'biblionumber'} = $biblionumber;
252 $item->{'biblioitemnumber'} = $biblioitemnumber;
253 $item->{cn_source} = delete $item->{'items.cn_source'}; # Because of C4::Biblio::_disambiguate
254 $item->{cn_sort} = delete $item->{'items.cn_sort'}; # Because of C4::Biblio::_disambiguate
256 # check for duplicate barcode
257 my %item_errors = CheckItemPreSave($item);
258 if (%item_errors) {
259 push @errors, _repack_item_errors($item_sequence_num, $item, \%item_errors);
260 push @bad_item_fields, $item_field;
261 next ITEMFIELD;
264 my $item_object = Koha::Item->new($item)->store;
265 push @itemnumbers, $item_object->itemnumber; # FIXME not checking error
267 logaction("CATALOGUING", "ADD", $item_object->itemnumber, "item") if C4::Context->preference("CataloguingLog");
269 my $new_item_marc = _marc_from_item_hash($item_object->unblessed, $frameworkcode, $unlinked_item_subfields);
270 $item_field->replace_with($new_item_marc->field($itemtag));
273 # remove any MARC item fields for rejected items
274 foreach my $item_field (@bad_item_fields) {
275 $record->delete_field($item_field);
278 # update the MARC biblio
279 # $biblionumber = ModBiblioMarc( $record, $biblionumber, $frameworkcode );
281 return (\@itemnumbers, \@errors);
284 sub ModItemFromMarc {
285 my $item_marc = shift;
286 my $biblionumber = shift;
287 my $itemnumber = shift;
289 my $frameworkcode = C4::Biblio::GetFrameworkCode($biblionumber);
290 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
292 my $localitemmarc = MARC::Record->new;
293 $localitemmarc->append_fields( $item_marc->field($itemtag) );
294 my $item_object = Koha::Items->find($itemnumber);
295 my $item = TransformMarcToKoha( $localitemmarc, $frameworkcode, 'items' );
296 $item->{cn_source} = delete $item->{'items.cn_source'}; # Because of C4::Biblio::_disambiguate
297 $item->{cn_sort} = delete $item->{'items.cn_sort'}; # Because of C4::Biblio::_disambiguate
298 $item->{itemnumber} = $itemnumber;
299 $item->{biblionumber} = $biblionumber;
300 $item_object = $item_object->set_or_blank($item);
301 my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
302 $item_object->more_subfields_xml(_get_unlinked_subfields_xml($unlinked_item_subfields))->store;
303 $item_object->store;
305 return $item_object->unblessed;
308 =head2 ModItemTransfer
310 ModItemTransfer($itemnumber, $frombranch, $tobranch, $trigger);
312 Marks an item as being transferred from one branch to another and records the trigger.
314 =cut
316 sub ModItemTransfer {
317 my ( $itemnumber, $frombranch, $tobranch, $trigger ) = @_;
319 my $dbh = C4::Context->dbh;
320 my $item = Koha::Items->find( $itemnumber );
322 # Remove the 'shelving cart' location status if it is being used.
323 CartToShelf( $itemnumber ) if $item->location && $item->location eq 'CART' && ( !$item->permanent_location || $item->permanent_location ne 'CART' );
325 $dbh->do("UPDATE branchtransfers SET datearrived = NOW(), comments = ? WHERE itemnumber = ? AND datearrived IS NULL", undef, "Canceled, new transfer from $frombranch to $tobranch created", $itemnumber);
327 #new entry in branchtransfers....
328 my $sth = $dbh->prepare(
329 "INSERT INTO branchtransfers (itemnumber, frombranch, datesent, tobranch, reason)
330 VALUES (?, ?, NOW(), ?, ?)");
331 $sth->execute($itemnumber, $frombranch, $tobranch, $trigger);
333 # FIXME we are fetching the item twice in the 2 next statements!
334 Koha::Items->find($itemnumber)->holdingbranch($frombranch)->store({ log_action => 0 });
335 ModDateLastSeen($itemnumber);
336 return;
339 =head2 ModDateLastSeen
341 ModDateLastSeen( $itemnumber, $leave_item_lost );
343 Mark item as seen. Is called when an item is issued, returned or manually marked during inventory/stocktaking.
344 C<$itemnumber> is the item number
345 C<$leave_item_lost> determines if a lost item will be found or remain lost
347 =cut
349 sub ModDateLastSeen {
350 my ( $itemnumber, $leave_item_lost ) = @_;
352 my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
354 my $item = Koha::Items->find($itemnumber);
355 $item->datelastseen($today);
356 $item->itemlost(0) unless $leave_item_lost;
357 $item->store({ log_action => 0 });
360 =head2 CheckItemPreSave
362 my $item_ref = TransformMarcToKoha($marc, 'items');
363 # do stuff
364 my %errors = CheckItemPreSave($item_ref);
365 if (exists $errors{'duplicate_barcode'}) {
366 print "item has duplicate barcode: ", $errors{'duplicate_barcode'}, "\n";
367 } elsif (exists $errors{'invalid_homebranch'}) {
368 print "item has invalid home branch: ", $errors{'invalid_homebranch'}, "\n";
369 } elsif (exists $errors{'invalid_holdingbranch'}) {
370 print "item has invalid holding branch: ", $errors{'invalid_holdingbranch'}, "\n";
371 } else {
372 print "item is OK";
375 Given a hashref containing item fields, determine if it can be
376 inserted or updated in the database. Specifically, checks for
377 database integrity issues, and returns a hash containing any
378 of the following keys, if applicable.
380 =over 2
382 =item duplicate_barcode
384 Barcode, if it duplicates one already found in the database.
386 =item invalid_homebranch
388 Home branch, if not defined in branches table.
390 =item invalid_holdingbranch
392 Holding branch, if not defined in branches table.
394 =back
396 This function does NOT implement any policy-related checks,
397 e.g., whether current operator is allowed to save an
398 item that has a given branch code.
400 =cut
402 sub CheckItemPreSave {
403 my $item_ref = shift;
405 my %errors = ();
407 # check for duplicate barcode
408 if (exists $item_ref->{'barcode'} and defined $item_ref->{'barcode'}) {
409 my $existing_item= Koha::Items->find({barcode => $item_ref->{'barcode'}});
410 if ($existing_item) {
411 if (!exists $item_ref->{'itemnumber'} # new item
412 or $item_ref->{'itemnumber'} != $existing_item->itemnumber) { # existing item
413 $errors{'duplicate_barcode'} = $item_ref->{'barcode'};
418 # check for valid home branch
419 if (exists $item_ref->{'homebranch'} and defined $item_ref->{'homebranch'}) {
420 my $home_library = Koha::Libraries->find( $item_ref->{homebranch} );
421 unless (defined $home_library) {
422 $errors{'invalid_homebranch'} = $item_ref->{'homebranch'};
426 # check for valid holding branch
427 if (exists $item_ref->{'holdingbranch'} and defined $item_ref->{'holdingbranch'}) {
428 my $holding_library = Koha::Libraries->find( $item_ref->{holdingbranch} );
429 unless (defined $holding_library) {
430 $errors{'invalid_holdingbranch'} = $item_ref->{'holdingbranch'};
434 return %errors;
438 =head1 EXPORTED SPECIAL ACCESSOR FUNCTIONS
440 The following functions provide various ways of
441 getting an item record, a set of item records, or
442 lists of authorized values for certain item fields.
444 =cut
446 =head2 GetItemsForInventory
448 ($itemlist, $iTotalRecords) = GetItemsForInventory( {
449 minlocation => $minlocation,
450 maxlocation => $maxlocation,
451 location => $location,
452 itemtype => $itemtype,
453 ignoreissued => $ignoreissued,
454 datelastseen => $datelastseen,
455 branchcode => $branchcode,
456 branch => $branch,
457 offset => $offset,
458 size => $size,
459 statushash => $statushash,
460 } );
462 Retrieve a list of title/authors/barcode/callnumber, for biblio inventory.
464 The sub returns a reference to a list of hashes, each containing
465 itemnumber, author, title, barcode, item callnumber, and date last
466 seen. It is ordered by callnumber then title.
468 The required minlocation & maxlocation parameters are used to specify a range of item callnumbers
469 the datelastseen can be used to specify that you want to see items not seen since a past date only.
470 offset & size can be used to retrieve only a part of the whole listing (defaut behaviour)
471 $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.
473 $iTotalRecords is the number of rows that would have been returned without the $offset, $size limit clause
475 =cut
477 sub GetItemsForInventory {
478 my ( $parameters ) = @_;
479 my $minlocation = $parameters->{'minlocation'} // '';
480 my $maxlocation = $parameters->{'maxlocation'} // '';
481 my $class_source = $parameters->{'class_source'} // C4::Context->preference('DefaultClassificationSource');
482 my $location = $parameters->{'location'} // '';
483 my $itemtype = $parameters->{'itemtype'} // '';
484 my $ignoreissued = $parameters->{'ignoreissued'} // '';
485 my $datelastseen = $parameters->{'datelastseen'} // '';
486 my $branchcode = $parameters->{'branchcode'} // '';
487 my $branch = $parameters->{'branch'} // '';
488 my $offset = $parameters->{'offset'} // '';
489 my $size = $parameters->{'size'} // '';
490 my $statushash = $parameters->{'statushash'} // '';
491 my $ignore_waiting_holds = $parameters->{'ignore_waiting_holds'} // '';
493 my $dbh = C4::Context->dbh;
494 my ( @bind_params, @where_strings );
496 my $min_cnsort = GetClassSort($class_source,undef,$minlocation);
497 my $max_cnsort = GetClassSort($class_source,undef,$maxlocation);
499 my $select_columns = q{
500 SELECT DISTINCT(items.itemnumber), barcode, itemcallnumber, title, author, biblio.biblionumber, biblio.frameworkcode, datelastseen, homebranch, location, notforloan, damaged, itemlost, withdrawn, stocknumber, items.cn_sort
502 my $select_count = q{SELECT COUNT(DISTINCT(items.itemnumber))};
503 my $query = q{
504 FROM items
505 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
506 LEFT JOIN biblioitems on items.biblionumber = biblioitems.biblionumber
508 if ($statushash){
509 for my $authvfield (keys %$statushash){
510 if ( scalar @{$statushash->{$authvfield}} > 0 ){
511 my $joinedvals = join ',', @{$statushash->{$authvfield}};
512 push @where_strings, "$authvfield in (" . $joinedvals . ")";
517 if ($minlocation) {
518 push @where_strings, 'items.cn_sort >= ?';
519 push @bind_params, $min_cnsort;
522 if ($maxlocation) {
523 push @where_strings, 'items.cn_sort <= ?';
524 push @bind_params, $max_cnsort;
527 if ($datelastseen) {
528 $datelastseen = output_pref({ str => $datelastseen, dateformat => 'iso', dateonly => 1 });
529 push @where_strings, '(datelastseen < ? OR datelastseen IS NULL)';
530 push @bind_params, $datelastseen;
533 if ( $location ) {
534 push @where_strings, 'items.location = ?';
535 push @bind_params, $location;
538 if ( $branchcode ) {
539 if($branch eq "homebranch"){
540 push @where_strings, 'items.homebranch = ?';
541 }else{
542 push @where_strings, 'items.holdingbranch = ?';
544 push @bind_params, $branchcode;
547 if ( $itemtype ) {
548 push @where_strings, 'biblioitems.itemtype = ?';
549 push @bind_params, $itemtype;
552 if ( $ignoreissued) {
553 $query .= "LEFT JOIN issues ON items.itemnumber = issues.itemnumber ";
554 push @where_strings, 'issues.date_due IS NULL';
557 if ( $ignore_waiting_holds ) {
558 $query .= "LEFT JOIN reserves ON items.itemnumber = reserves.itemnumber ";
559 push( @where_strings, q{(reserves.found != 'W' OR reserves.found IS NULL)} );
562 if ( @where_strings ) {
563 $query .= 'WHERE ';
564 $query .= join ' AND ', @where_strings;
566 my $count_query = $select_count . $query;
567 $query .= ' ORDER BY items.cn_sort, itemcallnumber, title';
568 $query .= " LIMIT $offset, $size" if ($offset and $size);
569 $query = $select_columns . $query;
570 my $sth = $dbh->prepare($query);
571 $sth->execute( @bind_params );
573 my @results = ();
574 my $tmpresults = $sth->fetchall_arrayref({});
575 $sth = $dbh->prepare( $count_query );
576 $sth->execute( @bind_params );
577 my ($iTotalRecords) = $sth->fetchrow_array();
579 my @avs = Koha::AuthorisedValues->search(
580 { 'marc_subfield_structures.kohafield' => { '>' => '' },
581 'me.authorised_value' => { '>' => '' },
583 { join => { category => 'marc_subfield_structures' },
584 distinct => ['marc_subfield_structures.kohafield, me.category, frameworkcode, me.authorised_value'],
585 '+select' => [ 'marc_subfield_structures.kohafield', 'marc_subfield_structures.frameworkcode', 'me.authorised_value', 'me.lib' ],
586 '+as' => [ 'kohafield', 'frameworkcode', 'authorised_value', 'lib' ],
590 my $avmapping = { map { $_->get_column('kohafield') . ',' . $_->get_column('frameworkcode') . ',' . $_->get_column('authorised_value') => $_->get_column('lib') } @avs };
592 foreach my $row (@$tmpresults) {
594 # Auth values
595 foreach (keys %$row) {
596 if (
597 defined(
598 $avmapping->{ "items.$_," . $row->{'frameworkcode'} . "," . ( $row->{$_} // q{} ) }
601 $row->{$_} = $avmapping->{"items.$_,".$row->{'frameworkcode'}.",".$row->{$_}};
604 push @results, $row;
607 return (\@results, $iTotalRecords);
610 =head2 GetItemsInfo
612 @results = GetItemsInfo($biblionumber);
614 Returns information about items with the given biblionumber.
616 C<GetItemsInfo> returns a list of references-to-hash. Each element
617 contains a number of keys. Most of them are attributes from the
618 C<biblio>, C<biblioitems>, C<items>, and C<itemtypes> tables in the
619 Koha database. Other keys include:
621 =over 2
623 =item C<$data-E<gt>{branchname}>
625 The name (not the code) of the branch to which the book belongs.
627 =item C<$data-E<gt>{datelastseen}>
629 This is simply C<items.datelastseen>, except that while the date is
630 stored in YYYY-MM-DD format in the database, here it is converted to
631 DD/MM/YYYY format. A NULL date is returned as C<//>.
633 =item C<$data-E<gt>{datedue}>
635 =item C<$data-E<gt>{class}>
637 This is the concatenation of C<biblioitems.classification>, the book's
638 Dewey code, and C<biblioitems.subclass>.
640 =item C<$data-E<gt>{ocount}>
642 I think this is the number of copies of the book available.
644 =item C<$data-E<gt>{order}>
646 If this is set, it is set to C<One Order>.
648 =back
650 =cut
652 sub GetItemsInfo {
653 my ( $biblionumber ) = @_;
654 my $dbh = C4::Context->dbh;
655 require C4::Languages;
656 my $language = C4::Languages::getlanguage();
657 my $query = "
658 SELECT items.*,
659 biblio.*,
660 biblioitems.volume,
661 biblioitems.number,
662 biblioitems.itemtype,
663 biblioitems.isbn,
664 biblioitems.issn,
665 biblioitems.publicationyear,
666 biblioitems.publishercode,
667 biblioitems.volumedate,
668 biblioitems.volumedesc,
669 biblioitems.lccn,
670 biblioitems.url,
671 items.notforloan as itemnotforloan,
672 issues.borrowernumber,
673 issues.date_due as datedue,
674 issues.onsite_checkout,
675 borrowers.cardnumber,
676 borrowers.surname,
677 borrowers.firstname,
678 borrowers.branchcode as bcode,
679 serial.serialseq,
680 serial.publisheddate,
681 itemtypes.description,
682 COALESCE( localization.translation, itemtypes.description ) AS translated_description,
683 itemtypes.notforloan as notforloan_per_itemtype,
684 holding.branchurl,
685 holding.branchcode,
686 holding.branchname,
687 holding.opac_info as holding_branch_opac_info,
688 home.opac_info as home_branch_opac_info,
689 IF(tmp_holdsqueue.itemnumber,1,0) AS has_pending_hold
690 FROM items
691 LEFT JOIN branches AS holding ON items.holdingbranch = holding.branchcode
692 LEFT JOIN branches AS home ON items.homebranch=home.branchcode
693 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
694 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
695 LEFT JOIN issues USING (itemnumber)
696 LEFT JOIN borrowers USING (borrowernumber)
697 LEFT JOIN serialitems USING (itemnumber)
698 LEFT JOIN serial USING (serialid)
699 LEFT JOIN itemtypes ON itemtypes.itemtype = "
700 . (C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype');
701 $query .= q|
702 LEFT JOIN tmp_holdsqueue USING (itemnumber)
703 LEFT JOIN localization ON itemtypes.itemtype = localization.code
704 AND localization.entity = 'itemtypes'
705 AND localization.lang = ?
708 $query .= " WHERE items.biblionumber = ? ORDER BY home.branchname, items.enumchron, LPAD( items.copynumber, 8, '0' ), items.dateaccessioned DESC" ;
709 my $sth = $dbh->prepare($query);
710 $sth->execute($language, $biblionumber);
711 my $i = 0;
712 my @results;
713 my $serial;
715 my $userenv = C4::Context->userenv;
716 my $want_not_same_branch = C4::Context->preference("IndependentBranches") && !C4::Context->IsSuperLibrarian();
717 while ( my $data = $sth->fetchrow_hashref ) {
718 if ( $data->{borrowernumber} && $want_not_same_branch) {
719 $data->{'NOTSAMEBRANCH'} = $data->{'bcode'} ne $userenv->{branch};
722 $serial ||= $data->{'serial'};
724 my $descriptions;
725 # get notforloan complete status if applicable
726 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.notforloan', authorised_value => $data->{itemnotforloan} });
727 $data->{notforloanvalue} = $descriptions->{lib} // '';
728 $data->{notforloanvalueopac} = $descriptions->{opac_description} // '';
730 # get restricted status and description if applicable
731 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.restricted', authorised_value => $data->{restricted} });
732 $data->{restrictedvalue} = $descriptions->{lib} // '';
733 $data->{restrictedvalueopac} = $descriptions->{opac_description} // '';
735 # my stack procedures
736 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.stack', authorised_value => $data->{stack} });
737 $data->{stack} = $descriptions->{lib} // '';
739 # Find the last 3 people who borrowed this item.
740 my $sth2 = $dbh->prepare("SELECT * FROM old_issues,borrowers
741 WHERE itemnumber = ?
742 AND old_issues.borrowernumber = borrowers.borrowernumber
743 ORDER BY returndate DESC
744 LIMIT 3");
745 $sth2->execute($data->{'itemnumber'});
746 my $ii = 0;
747 while (my $data2 = $sth2->fetchrow_hashref()) {
748 $data->{"timestamp$ii"} = $data2->{'timestamp'} if $data2->{'timestamp'};
749 $data->{"card$ii"} = $data2->{'cardnumber'} if $data2->{'cardnumber'};
750 $data->{"borrower$ii"} = $data2->{'borrowernumber'} if $data2->{'borrowernumber'};
751 $ii++;
754 $results[$i] = $data;
755 $i++;
758 return $serial
759 ? sort { ($b->{'publisheddate'} || $b->{'enumchron'}) cmp ($a->{'publisheddate'} || $a->{'enumchron'}) } @results
760 : @results;
763 =head2 GetItemsLocationInfo
765 my @itemlocinfo = GetItemsLocationInfo($biblionumber);
767 Returns the branch names, shelving location and itemcallnumber for each item attached to the biblio in question
769 C<GetItemsInfo> returns a list of references-to-hash. Data returned:
771 =over 2
773 =item C<$data-E<gt>{homebranch}>
775 Branch Name of the item's homebranch
777 =item C<$data-E<gt>{holdingbranch}>
779 Branch Name of the item's holdingbranch
781 =item C<$data-E<gt>{location}>
783 Item's shelving location code
785 =item C<$data-E<gt>{location_intranet}>
787 The intranet description for the Shelving Location as set in authorised_values 'LOC'
789 =item C<$data-E<gt>{location_opac}>
791 The OPAC description for the Shelving Location as set in authorised_values 'LOC'. Falls back to intranet description if no OPAC
792 description is set.
794 =item C<$data-E<gt>{itemcallnumber}>
796 Item's itemcallnumber
798 =item C<$data-E<gt>{cn_sort}>
800 Item's call number normalized for sorting
802 =back
804 =cut
806 sub GetItemsLocationInfo {
807 my $biblionumber = shift;
808 my @results;
810 my $dbh = C4::Context->dbh;
811 my $query = "SELECT a.branchname as homebranch, b.branchname as holdingbranch,
812 location, itemcallnumber, cn_sort
813 FROM items, branches as a, branches as b
814 WHERE homebranch = a.branchcode AND holdingbranch = b.branchcode
815 AND biblionumber = ?
816 ORDER BY cn_sort ASC";
817 my $sth = $dbh->prepare($query);
818 $sth->execute($biblionumber);
820 while ( my $data = $sth->fetchrow_hashref ) {
821 my $av = Koha::AuthorisedValues->search({ category => 'LOC', authorised_value => $data->{location} });
822 $av = $av->count ? $av->next : undef;
823 $data->{location_intranet} = $av ? $av->lib : '';
824 $data->{location_opac} = $av ? $av->opac_description : '';
825 push @results, $data;
827 return @results;
830 =head2 GetHostItemsInfo
832 $hostiteminfo = GetHostItemsInfo($hostfield);
833 Returns the iteminfo for items linked to records via a host field
835 =cut
837 sub GetHostItemsInfo {
838 my ($record) = @_;
839 my @returnitemsInfo;
841 if( !C4::Context->preference('EasyAnalyticalRecords') ) {
842 return @returnitemsInfo;
845 my @fields;
846 if( C4::Context->preference('marcflavour') eq 'MARC21' ||
847 C4::Context->preference('marcflavour') eq 'NORMARC') {
848 @fields = $record->field('773');
849 } elsif( C4::Context->preference('marcflavour') eq 'UNIMARC') {
850 @fields = $record->field('461');
853 foreach my $hostfield ( @fields ) {
854 my $hostbiblionumber = $hostfield->subfield("0");
855 my $linkeditemnumber = $hostfield->subfield("9");
856 my @hostitemInfos = GetItemsInfo($hostbiblionumber);
857 foreach my $hostitemInfo (@hostitemInfos) {
858 if( $hostitemInfo->{itemnumber} eq $linkeditemnumber ) {
859 push @returnitemsInfo, $hostitemInfo;
860 last;
864 return @returnitemsInfo;
867 =head2 get_hostitemnumbers_of
869 my @itemnumbers_of = get_hostitemnumbers_of($biblionumber);
871 Given a biblionumber, return the list of corresponding itemnumbers that are linked to it via host fields
873 Return a reference on a hash where key is a biblionumber and values are
874 references on array of itemnumbers.
876 =cut
879 sub get_hostitemnumbers_of {
880 my ($biblionumber) = @_;
882 if( !C4::Context->preference('EasyAnalyticalRecords') ) {
883 return ();
886 my $marcrecord = C4::Biblio::GetMarcBiblio({ biblionumber => $biblionumber });
887 return unless $marcrecord;
889 my ( @returnhostitemnumbers, $tag, $biblio_s, $item_s );
891 my $marcflavor = C4::Context->preference('marcflavour');
892 if ( $marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC' ) {
893 $tag = '773';
894 $biblio_s = '0';
895 $item_s = '9';
897 elsif ( $marcflavor eq 'UNIMARC' ) {
898 $tag = '461';
899 $biblio_s = '0';
900 $item_s = '9';
903 foreach my $hostfield ( $marcrecord->field($tag) ) {
904 my $hostbiblionumber = $hostfield->subfield($biblio_s);
905 next unless $hostbiblionumber; # have tag, don't have $biblio_s subfield
906 my $linkeditemnumber = $hostfield->subfield($item_s);
907 if ( ! $linkeditemnumber ) {
908 warn "ERROR biblionumber $biblionumber has 773^0, but doesn't have 9";
909 next;
911 my $is_from_biblio = Koha::Items->search({ itemnumber => $linkeditemnumber, biblionumber => $hostbiblionumber });
912 push @returnhostitemnumbers, $linkeditemnumber
913 if $is_from_biblio;
916 return @returnhostitemnumbers;
919 =head2 GetHiddenItemnumbers
921 my @itemnumbers_to_hide = GetHiddenItemnumbers({ items => \@items, borcat => $category });
923 Given a list of items it checks which should be hidden from the OPAC given
924 the current configuration. Returns a list of itemnumbers corresponding to
925 those that should be hidden. Optionally takes a borcat parameter for certain borrower types
926 to be excluded
928 =cut
930 sub GetHiddenItemnumbers {
931 my $params = shift;
932 my $items = $params->{items};
933 if (my $exceptions = C4::Context->preference('OpacHiddenItemsExceptions') and $params->{'borcat'}){
934 foreach my $except (split(/\|/, $exceptions)){
935 if ($params->{'borcat'} eq $except){
936 return; # we don't hide anything for this borrower category
940 my @resultitems;
942 my $yaml = C4::Context->preference('OpacHiddenItems');
943 return () if (! $yaml =~ /\S/ );
944 $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
945 my $hidingrules;
946 eval {
947 $hidingrules = YAML::Load($yaml);
949 if ($@) {
950 warn "Unable to parse OpacHiddenItems syspref : $@";
951 return ();
953 my $dbh = C4::Context->dbh;
955 # For each item
956 foreach my $item (@$items) {
958 # We check each rule
959 foreach my $field (keys %$hidingrules) {
960 my $val;
961 if (exists $item->{$field}) {
962 $val = $item->{$field};
964 else {
965 my $query = "SELECT $field from items where itemnumber = ?";
966 $val = $dbh->selectrow_array($query, undef, $item->{'itemnumber'});
968 $val = '' unless defined $val;
970 # If the results matches the values in the yaml file
971 if (any { $val eq $_ } @{$hidingrules->{$field}}) {
973 # We add the itemnumber to the list
974 push @resultitems, $item->{'itemnumber'};
976 # If at least one rule matched for an item, no need to test the others
977 last;
981 return @resultitems;
984 =head1 LIMITED USE FUNCTIONS
986 The following functions, while part of the public API,
987 are not exported. This is generally because they are
988 meant to be used by only one script for a specific
989 purpose, and should not be used in any other context
990 without careful thought.
992 =cut
994 =head2 GetMarcItem
996 my $item_marc = GetMarcItem($biblionumber, $itemnumber);
998 Returns MARC::Record of the item passed in parameter.
999 This function is meant for use only in C<cataloguing/additem.pl>,
1000 where it is needed to support that script's MARC-like
1001 editor.
1003 =cut
1005 sub GetMarcItem {
1006 my ( $biblionumber, $itemnumber ) = @_;
1008 # GetMarcItem has been revised so that it does the following:
1009 # 1. Gets the item information from the items table.
1010 # 2. Converts it to a MARC field for storage in the bib record.
1012 # The previous behavior was:
1013 # 1. Get the bib record.
1014 # 2. Return the MARC tag corresponding to the item record.
1016 # The difference is that one treats the items row as authoritative,
1017 # while the other treats the MARC representation as authoritative
1018 # under certain circumstances.
1020 my $item = Koha::Items->find($itemnumber) or return;
1022 # Tack on 'items.' prefix to column names so that C4::Biblio::TransformKohaToMarc will work.
1023 # Also, don't emit a subfield if the underlying field is blank.
1025 return Item2Marc($item->unblessed, $biblionumber);
1028 sub Item2Marc {
1029 my ($itemrecord,$biblionumber)=@_;
1030 my $mungeditem = {
1031 map {
1032 defined($itemrecord->{$_}) && $itemrecord->{$_} ne '' ? ("items.$_" => $itemrecord->{$_}) : ()
1033 } keys %{ $itemrecord }
1035 my $framework = C4::Biblio::GetFrameworkCode( $biblionumber );
1036 my $itemmarc = C4::Biblio::TransformKohaToMarc( $mungeditem, { framework => $framework } );
1037 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField(
1038 "items.itemnumber", $framework,
1041 my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($mungeditem->{'items.more_subfields_xml'});
1042 if (defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1) {
1043 foreach my $field ($itemmarc->field($itemtag)){
1044 $field->add_subfields(@$unlinked_item_subfields);
1047 return $itemmarc;
1050 =head1 PRIVATE FUNCTIONS AND VARIABLES
1052 The following functions are not meant to be called
1053 directly, but are documented in order to explain
1054 the inner workings of C<C4::Items>.
1056 =cut
1058 =head2 MoveItemFromBiblio
1060 MoveItemFromBiblio($itenumber, $frombiblio, $tobiblio);
1062 Moves an item from a biblio to another
1064 Returns undef if the move failed or the biblionumber of the destination record otherwise
1066 =cut
1068 sub MoveItemFromBiblio {
1069 my ($itemnumber, $frombiblio, $tobiblio) = @_;
1070 my $dbh = C4::Context->dbh;
1071 my ( $tobiblioitem ) = $dbh->selectrow_array(q|
1072 SELECT biblioitemnumber
1073 FROM biblioitems
1074 WHERE biblionumber = ?
1075 |, undef, $tobiblio );
1076 my $return = $dbh->do(q|
1077 UPDATE items
1078 SET biblioitemnumber = ?,
1079 biblionumber = ?
1080 WHERE itemnumber = ?
1081 AND biblionumber = ?
1082 |, undef, $tobiblioitem, $tobiblio, $itemnumber, $frombiblio );
1083 if ($return == 1) {
1084 ModZebra( $tobiblio, "specialUpdate", "biblioserver" );
1085 ModZebra( $frombiblio, "specialUpdate", "biblioserver" );
1086 # Checking if the item we want to move is in an order
1087 require C4::Acquisition;
1088 my $order = C4::Acquisition::GetOrderFromItemnumber($itemnumber);
1089 if ($order) {
1090 # Replacing the biblionumber within the order if necessary
1091 $order->{'biblionumber'} = $tobiblio;
1092 C4::Acquisition::ModOrder($order);
1095 # Update reserves, hold_fill_targets, tmp_holdsqueue and linktracker tables
1096 for my $table_name ( qw( reserves hold_fill_targets tmp_holdsqueue linktracker ) ) {
1097 $dbh->do( qq|
1098 UPDATE $table_name
1099 SET biblionumber = ?
1100 WHERE itemnumber = ?
1101 |, undef, $tobiblio, $itemnumber );
1103 return $tobiblio;
1105 return;
1108 =head2 _marc_from_item_hash
1110 my $item_marc = _marc_from_item_hash($item, $frameworkcode[, $unlinked_item_subfields]);
1112 Given an item hash representing a complete item record,
1113 create a C<MARC::Record> object containing an embedded
1114 tag representing that item.
1116 The third, optional parameter C<$unlinked_item_subfields> is
1117 an arrayref of subfields (not mapped to C<items> fields per the
1118 framework) to be added to the MARC representation
1119 of the item.
1121 =cut
1123 sub _marc_from_item_hash {
1124 my $item = shift;
1125 my $frameworkcode = shift;
1126 my $unlinked_item_subfields;
1127 if (@_) {
1128 $unlinked_item_subfields = shift;
1131 # Tack on 'items.' prefix to column names so lookup from MARC frameworks will work
1132 # Also, don't emit a subfield if the underlying field is blank.
1133 my $mungeditem = { map { (defined($item->{$_}) and $item->{$_} ne '') ?
1134 (/^items\./ ? ($_ => $item->{$_}) : ("items.$_" => $item->{$_}))
1135 : () } keys %{ $item } };
1137 my $item_marc = MARC::Record->new();
1138 foreach my $item_field ( keys %{$mungeditem} ) {
1139 my ( $tag, $subfield ) = C4::Biblio::GetMarcFromKohaField( $item_field );
1140 next unless defined $tag and defined $subfield; # skip if not mapped to MARC field
1141 my @values = split(/\s?\|\s?/, $mungeditem->{$item_field}, -1);
1142 foreach my $value (@values){
1143 if ( my $field = $item_marc->field($tag) ) {
1144 $field->add_subfields( $subfield => $value );
1145 } else {
1146 my $add_subfields = [];
1147 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
1148 $add_subfields = $unlinked_item_subfields;
1150 $item_marc->add_fields( $tag, " ", " ", $subfield => $value, @$add_subfields );
1155 return $item_marc;
1158 =head2 _repack_item_errors
1160 Add an error message hash generated by C<CheckItemPreSave>
1161 to a list of errors.
1163 =cut
1165 sub _repack_item_errors {
1166 my $item_sequence_num = shift;
1167 my $item_ref = shift;
1168 my $error_ref = shift;
1170 my @repacked_errors = ();
1172 foreach my $error_code (sort keys %{ $error_ref }) {
1173 my $repacked_error = {};
1174 $repacked_error->{'item_sequence'} = $item_sequence_num;
1175 $repacked_error->{'item_barcode'} = exists($item_ref->{'barcode'}) ? $item_ref->{'barcode'} : '';
1176 $repacked_error->{'error_code'} = $error_code;
1177 $repacked_error->{'error_information'} = $error_ref->{$error_code};
1178 push @repacked_errors, $repacked_error;
1181 return @repacked_errors;
1184 =head2 _get_unlinked_item_subfields
1186 my $unlinked_item_subfields = _get_unlinked_item_subfields($original_item_marc, $frameworkcode);
1188 =cut
1190 sub _get_unlinked_item_subfields {
1191 my $original_item_marc = shift;
1192 my $frameworkcode = shift;
1194 my $marcstructure = C4::Biblio::GetMarcStructure(1, $frameworkcode, { unsafe => 1 });
1196 # assume that this record has only one field, and that that
1197 # field contains only the item information
1198 my $subfields = [];
1199 my @fields = $original_item_marc->fields();
1200 if ($#fields > -1) {
1201 my $field = $fields[0];
1202 my $tag = $field->tag();
1203 foreach my $subfield ($field->subfields()) {
1204 if (defined $subfield->[1] and
1205 $subfield->[1] ne '' and
1206 !$marcstructure->{$tag}->{$subfield->[0]}->{'kohafield'}) {
1207 push @$subfields, $subfield->[0] => $subfield->[1];
1211 return $subfields;
1214 =head2 _get_unlinked_subfields_xml
1216 my $unlinked_subfields_xml = _get_unlinked_subfields_xml($unlinked_item_subfields);
1218 =cut
1220 sub _get_unlinked_subfields_xml {
1221 my $unlinked_item_subfields = shift;
1223 my $xml;
1224 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
1225 my $marc = MARC::Record->new();
1226 # use of tag 999 is arbitrary, and doesn't need to match the item tag
1227 # used in the framework
1228 $marc->append_fields(MARC::Field->new('999', ' ', ' ', @$unlinked_item_subfields));
1229 $marc->encoding("UTF-8");
1230 $xml = $marc->as_xml("USMARC");
1233 return $xml;
1236 =head2 _parse_unlinked_item_subfields_from_xml
1238 my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($whole_item->{'more_subfields_xml'}):
1240 =cut
1242 sub _parse_unlinked_item_subfields_from_xml {
1243 my $xml = shift;
1244 require C4::Charset;
1245 return unless defined $xml and $xml ne "";
1246 my $marc = MARC::Record->new_from_xml(C4::Charset::StripNonXmlChars($xml),'UTF-8');
1247 my $unlinked_subfields = [];
1248 my @fields = $marc->fields();
1249 if ($#fields > -1) {
1250 foreach my $subfield ($fields[0]->subfields()) {
1251 push @$unlinked_subfields, $subfield->[0] => $subfield->[1];
1254 return $unlinked_subfields;
1257 =head2 GetAnalyticsCount
1259 $count= &GetAnalyticsCount($itemnumber)
1261 counts Usage of itemnumber in Analytical bibliorecords.
1263 =cut
1265 sub GetAnalyticsCount {
1266 my ($itemnumber) = @_;
1268 ### ZOOM search here
1269 my $query;
1270 $query= "hi=".$itemnumber;
1271 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
1272 my ($err,$res,$result) = $searcher->simple_search_compat($query,0,10);
1273 return ($result);
1276 sub _SearchItems_build_where_fragment {
1277 my ($filter) = @_;
1279 my $dbh = C4::Context->dbh;
1281 my $where_fragment;
1282 if (exists($filter->{conjunction})) {
1283 my (@where_strs, @where_args);
1284 foreach my $f (@{ $filter->{filters} }) {
1285 my $fragment = _SearchItems_build_where_fragment($f);
1286 if ($fragment) {
1287 push @where_strs, $fragment->{str};
1288 push @where_args, @{ $fragment->{args} };
1291 my $where_str = '';
1292 if (@where_strs) {
1293 $where_str = '(' . join (' ' . $filter->{conjunction} . ' ', @where_strs) . ')';
1294 $where_fragment = {
1295 str => $where_str,
1296 args => \@where_args,
1299 } else {
1300 my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1301 push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1302 push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1303 my @operators = qw(= != > < >= <= like);
1304 my $field = $filter->{field} // q{};
1305 if ( (0 < grep { $_ eq $field } @columns) or (substr($field, 0, 5) eq 'marc:') ) {
1306 my $op = $filter->{operator};
1307 my $query = $filter->{query};
1309 if (!$op or (0 == grep { $_ eq $op } @operators)) {
1310 $op = '='; # default operator
1313 my $column;
1314 if ($field =~ /^marc:(\d{3})(?:\$(\w))?$/) {
1315 my $marcfield = $1;
1316 my $marcsubfield = $2;
1317 my ($kohafield) = $dbh->selectrow_array(q|
1318 SELECT kohafield FROM marc_subfield_structure
1319 WHERE tagfield=? AND tagsubfield=? AND frameworkcode=''
1320 |, undef, $marcfield, $marcsubfield);
1322 if ($kohafield) {
1323 $column = $kohafield;
1324 } else {
1325 # MARC field is not linked to a DB field so we need to use
1326 # ExtractValue on marcxml from biblio_metadata or
1327 # items.more_subfields_xml, depending on the MARC field.
1328 my $xpath;
1329 my $sqlfield;
1330 my ($itemfield) = C4::Biblio::GetMarcFromKohaField('items.itemnumber');
1331 if ($marcfield eq $itemfield) {
1332 $sqlfield = 'more_subfields_xml';
1333 $xpath = '//record/datafield/subfield[@code="' . $marcsubfield . '"]';
1334 } else {
1335 $sqlfield = 'metadata'; # From biblio_metadata
1336 if ($marcfield < 10) {
1337 $xpath = "//record/controlfield[\@tag=\"$marcfield\"]";
1338 } else {
1339 $xpath = "//record/datafield[\@tag=\"$marcfield\"]/subfield[\@code=\"$marcsubfield\"]";
1342 $column = "ExtractValue($sqlfield, '$xpath')";
1344 } elsif ($field eq 'issues') {
1345 # Consider NULL as 0 for issues count
1346 $column = 'COALESCE(issues,0)';
1347 } else {
1348 $column = $field;
1351 if (ref $query eq 'ARRAY') {
1352 if ($op eq '=') {
1353 $op = 'IN';
1354 } elsif ($op eq '!=') {
1355 $op = 'NOT IN';
1357 $where_fragment = {
1358 str => "$column $op (" . join (',', ('?') x @$query) . ")",
1359 args => $query,
1361 } else {
1362 $where_fragment = {
1363 str => "$column $op ?",
1364 args => [ $query ],
1370 return $where_fragment;
1373 =head2 SearchItems
1375 my ($items, $total) = SearchItems($filter, $params);
1377 Perform a search among items
1379 $filter is a reference to a hash which can be a filter, or a combination of filters.
1381 A filter has the following keys:
1383 =over 2
1385 =item * field: the name of a SQL column in table items
1387 =item * query: the value to search in this column
1389 =item * operator: comparison operator. Can be one of = != > < >= <= like
1391 =back
1393 A combination of filters hash the following keys:
1395 =over 2
1397 =item * conjunction: 'AND' or 'OR'
1399 =item * filters: array ref of filters
1401 =back
1403 $params is a reference to a hash that can contain the following parameters:
1405 =over 2
1407 =item * rows: Number of items to return. 0 returns everything (default: 0)
1409 =item * page: Page to return (return items from (page-1)*rows to (page*rows)-1)
1410 (default: 1)
1412 =item * sortby: A SQL column name in items table to sort on
1414 =item * sortorder: 'ASC' or 'DESC'
1416 =back
1418 =cut
1420 sub SearchItems {
1421 my ($filter, $params) = @_;
1423 $filter //= {};
1424 $params //= {};
1425 return unless ref $filter eq 'HASH';
1426 return unless ref $params eq 'HASH';
1428 # Default parameters
1429 $params->{rows} ||= 0;
1430 $params->{page} ||= 1;
1431 $params->{sortby} ||= 'itemnumber';
1432 $params->{sortorder} ||= 'ASC';
1434 my ($where_str, @where_args);
1435 my $where_fragment = _SearchItems_build_where_fragment($filter);
1436 if ($where_fragment) {
1437 $where_str = $where_fragment->{str};
1438 @where_args = @{ $where_fragment->{args} };
1441 my $dbh = C4::Context->dbh;
1442 my $query = q{
1443 SELECT SQL_CALC_FOUND_ROWS items.*
1444 FROM items
1445 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
1446 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
1447 LEFT JOIN biblio_metadata ON biblio_metadata.biblionumber = biblio.biblionumber
1448 WHERE 1
1450 if (defined $where_str and $where_str ne '') {
1451 $query .= qq{ AND $where_str };
1454 $query .= q{ AND biblio_metadata.format = 'marcxml' AND biblio_metadata.schema = ? };
1455 push @where_args, C4::Context->preference('marcflavour');
1457 my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1458 push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1459 push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1460 my $sortby = (0 < grep {$params->{sortby} eq $_} @columns)
1461 ? $params->{sortby} : 'itemnumber';
1462 my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
1463 $query .= qq{ ORDER BY $sortby $sortorder };
1465 my $rows = $params->{rows};
1466 my @limit_args;
1467 if ($rows > 0) {
1468 my $offset = $rows * ($params->{page}-1);
1469 $query .= qq { LIMIT ?, ? };
1470 push @limit_args, $offset, $rows;
1473 my $sth = $dbh->prepare($query);
1474 my $rv = $sth->execute(@where_args, @limit_args);
1476 return unless ($rv);
1477 my ($total_rows) = $dbh->selectrow_array(q{ SELECT FOUND_ROWS() });
1479 return ($sth->fetchall_arrayref({}), $total_rows);
1483 =head1 OTHER FUNCTIONS
1485 =head2 _find_value
1487 ($indicators, $value) = _find_value($tag, $subfield, $record,$encoding);
1489 Find the given $subfield in the given $tag in the given
1490 MARC::Record $record. If the subfield is found, returns
1491 the (indicators, value) pair; otherwise, (undef, undef) is
1492 returned.
1494 PROPOSITION :
1495 Such a function is used in addbiblio AND additem and serial-edit and maybe could be used in Authorities.
1496 I suggest we export it from this module.
1498 =cut
1500 sub _find_value {
1501 my ( $tagfield, $insubfield, $record, $encoding ) = @_;
1502 my @result;
1503 my $indicator;
1504 if ( $tagfield < 10 ) {
1505 if ( $record->field($tagfield) ) {
1506 push @result, $record->field($tagfield)->data();
1507 } else {
1508 push @result, "";
1510 } else {
1511 foreach my $field ( $record->field($tagfield) ) {
1512 my @subfields = $field->subfields();
1513 foreach my $subfield (@subfields) {
1514 if ( @$subfield[0] eq $insubfield ) {
1515 push @result, @$subfield[1];
1516 $indicator = $field->indicator(1) . $field->indicator(2);
1521 return ( $indicator, @result );
1525 =head2 PrepareItemrecordDisplay
1527 PrepareItemrecordDisplay($itemrecord,$bibnum,$itemumber,$frameworkcode);
1529 Returns a hash with all the fields for Display a given item data in a template
1531 The $frameworkcode returns the item for the given frameworkcode, ONLY if bibnum is not provided
1533 =cut
1535 sub PrepareItemrecordDisplay {
1537 my ( $bibnum, $itemnum, $defaultvalues, $frameworkcode ) = @_;
1539 my $dbh = C4::Context->dbh;
1540 $frameworkcode = C4::Biblio::GetFrameworkCode($bibnum) if $bibnum;
1541 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1543 # Note: $tagslib obtained from GetMarcStructure() in 'unsafe' mode is
1544 # a shared data structure. No plugin (including custom ones) should change
1545 # its contents. See also GetMarcStructure.
1546 my $tagslib = GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1548 # return nothing if we don't have found an existing framework.
1549 return q{} unless $tagslib;
1550 my $itemrecord;
1551 if ($itemnum) {
1552 $itemrecord = C4::Items::GetMarcItem( $bibnum, $itemnum );
1554 my @loop_data;
1556 my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
1557 my $query = qq{
1558 SELECT authorised_value,lib FROM authorised_values
1560 $query .= qq{
1561 LEFT JOIN authorised_values_branches ON ( id = av_id )
1562 } if $branch_limit;
1563 $query .= qq{
1564 WHERE category = ?
1566 $query .= qq{ AND ( branchcode = ? OR branchcode IS NULL )} if $branch_limit;
1567 $query .= qq{ ORDER BY lib};
1568 my $authorised_values_sth = $dbh->prepare( $query );
1569 foreach my $tag ( sort keys %{$tagslib} ) {
1570 if ( $tag ne '' ) {
1572 # loop through each subfield
1573 my $cntsubf;
1574 foreach my $subfield ( sort keys %{ $tagslib->{$tag} } ) {
1575 next if IsMarcStructureInternal($tagslib->{$tag}{$subfield});
1576 next unless ( $tagslib->{$tag}->{$subfield}->{'tab'} );
1577 next if ( $tagslib->{$tag}->{$subfield}->{'tab'} ne "10" );
1578 my %subfield_data;
1579 $subfield_data{tag} = $tag;
1580 $subfield_data{subfield} = $subfield;
1581 $subfield_data{countsubfield} = $cntsubf++;
1582 $subfield_data{kohafield} = $tagslib->{$tag}->{$subfield}->{'kohafield'};
1583 $subfield_data{id} = "tag_".$tag."_subfield_".$subfield."_".int(rand(1000000));
1585 # $subfield_data{marc_lib}=$tagslib->{$tag}->{$subfield}->{lib};
1586 $subfield_data{marc_lib} = $tagslib->{$tag}->{$subfield}->{lib};
1587 $subfield_data{mandatory} = $tagslib->{$tag}->{$subfield}->{mandatory};
1588 $subfield_data{repeatable} = $tagslib->{$tag}->{$subfield}->{repeatable};
1589 $subfield_data{hidden} = "display:none"
1590 if ( ( $tagslib->{$tag}->{$subfield}->{hidden} > 4 )
1591 || ( $tagslib->{$tag}->{$subfield}->{hidden} < -4 ) );
1592 my ( $x, $defaultvalue );
1593 if ($itemrecord) {
1594 ( $x, $defaultvalue ) = _find_value( $tag, $subfield, $itemrecord );
1596 $defaultvalue = $tagslib->{$tag}->{$subfield}->{defaultvalue} unless $defaultvalue;
1597 if ( !defined $defaultvalue ) {
1598 $defaultvalue = q||;
1599 } else {
1600 $defaultvalue =~ s/"/&quot;/g;
1603 my $maxlength = $tagslib->{$tag}->{$subfield}->{maxlength};
1605 # search for itemcallnumber if applicable
1606 if ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
1607 && C4::Context->preference('itemcallnumber') && $itemrecord) {
1608 foreach my $itemcn_pref (split(/,/,C4::Context->preference('itemcallnumber'))){
1609 my $CNtag = substr( $itemcn_pref, 0, 3 );
1610 next unless my $field = $itemrecord->field($CNtag);
1611 my $CNsubfields = substr( $itemcn_pref, 3 );
1612 $defaultvalue = $field->as_string( $CNsubfields, ' ');
1613 last if $defaultvalue;
1616 if ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
1617 && $defaultvalues
1618 && $defaultvalues->{'callnumber'} ) {
1619 if( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ){
1620 # if the item record exists, only use default value if the item has no callnumber
1621 $defaultvalue = $defaultvalues->{callnumber};
1622 } elsif ( !$itemrecord and $defaultvalues ) {
1623 # if the item record *doesn't* exists, always use the default value
1624 $defaultvalue = $defaultvalues->{callnumber};
1627 if ( ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.holdingbranch' || $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.homebranch' )
1628 && $defaultvalues
1629 && $defaultvalues->{'branchcode'} ) {
1630 if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
1631 $defaultvalue = $defaultvalues->{branchcode};
1634 if ( ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.location' )
1635 && $defaultvalues
1636 && $defaultvalues->{'location'} ) {
1638 if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
1639 # if the item record exists, only use default value if the item has no locationr
1640 $defaultvalue = $defaultvalues->{location};
1641 } elsif ( !$itemrecord and $defaultvalues ) {
1642 # if the item record *doesn't* exists, always use the default value
1643 $defaultvalue = $defaultvalues->{location};
1646 if ( $tagslib->{$tag}->{$subfield}->{authorised_value} ) {
1647 my @authorised_values;
1648 my %authorised_lib;
1650 # builds list, depending on authorised value...
1651 #---- branch
1652 if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "branches" ) {
1653 if ( ( C4::Context->preference("IndependentBranches") )
1654 && !C4::Context->IsSuperLibrarian() ) {
1655 my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches WHERE branchcode = ? ORDER BY branchname" );
1656 $sth->execute( C4::Context->userenv->{branch} );
1657 push @authorised_values, ""
1658 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1659 while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1660 push @authorised_values, $branchcode;
1661 $authorised_lib{$branchcode} = $branchname;
1663 } else {
1664 my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches ORDER BY branchname" );
1665 $sth->execute;
1666 push @authorised_values, ""
1667 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1668 while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1669 push @authorised_values, $branchcode;
1670 $authorised_lib{$branchcode} = $branchname;
1674 $defaultvalue = C4::Context->userenv ? C4::Context->userenv->{branch} : undef;
1675 if ( $defaultvalues and $defaultvalues->{branchcode} ) {
1676 $defaultvalue = $defaultvalues->{branchcode};
1679 #----- itemtypes
1680 } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "itemtypes" ) {
1681 my $itemtypes = Koha::ItemTypes->search_with_localization;
1682 push @authorised_values, ""
1683 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1684 while ( my $itemtype = $itemtypes->next ) {
1685 push @authorised_values, $itemtype->itemtype;
1686 $authorised_lib{$itemtype->itemtype} = $itemtype->translated_description;
1688 if ($defaultvalues && $defaultvalues->{'itemtype'}) {
1689 $defaultvalue = $defaultvalues->{'itemtype'};
1692 #---- class_sources
1693 } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "cn_source" ) {
1694 push @authorised_values, "" unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1696 my $class_sources = GetClassSources();
1697 my $default_source = $defaultvalue || C4::Context->preference("DefaultClassificationSource");
1699 foreach my $class_source (sort keys %$class_sources) {
1700 next unless $class_sources->{$class_source}->{'used'} or
1701 ($class_source eq $default_source);
1702 push @authorised_values, $class_source;
1703 $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
1706 $defaultvalue = $default_source;
1708 #---- "true" authorised value
1709 } else {
1710 $authorised_values_sth->execute(
1711 $tagslib->{$tag}->{$subfield}->{authorised_value},
1712 $branch_limit ? $branch_limit : ()
1714 push @authorised_values, ""
1715 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1716 while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
1717 push @authorised_values, $value;
1718 $authorised_lib{$value} = $lib;
1721 $subfield_data{marc_value} = {
1722 type => 'select',
1723 values => \@authorised_values,
1724 default => $defaultvalue // q{},
1725 labels => \%authorised_lib,
1727 } elsif ( $tagslib->{$tag}->{$subfield}->{value_builder} ) {
1728 # it is a plugin
1729 require Koha::FrameworkPlugin;
1730 my $plugin = Koha::FrameworkPlugin->new({
1731 name => $tagslib->{$tag}->{$subfield}->{value_builder},
1732 item_style => 1,
1734 my $pars = { dbh => $dbh, record => undef, tagslib =>$tagslib, id => $subfield_data{id}, tabloop => undef };
1735 $plugin->build( $pars );
1736 if ( $itemrecord and my $field = $itemrecord->field($tag) ) {
1737 $defaultvalue = $field->subfield($subfield) || q{};
1739 if( !$plugin->errstr ) {
1740 #TODO Move html to template; see report 12176/13397
1741 my $tab= $plugin->noclick? '-1': '';
1742 my $class= $plugin->noclick? ' disabled': '';
1743 my $title= $plugin->noclick? 'No popup': 'Tag editor';
1744 $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;
1745 } else {
1746 warn $plugin->errstr;
1747 $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
1750 elsif ( $tag eq '' ) { # it's an hidden field
1751 $subfield_data{marc_value} = qq(<input type="hidden" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1753 elsif ( $tagslib->{$tag}->{$subfield}->{'hidden'} ) { # FIXME: shouldn't input type be "hidden" ?
1754 $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1756 elsif ( length($defaultvalue) > 100
1757 or (C4::Context->preference("marcflavour") eq "UNIMARC" and
1758 300 <= $tag && $tag < 400 && $subfield eq 'a' )
1759 or (C4::Context->preference("marcflavour") eq "MARC21" and
1760 500 <= $tag && $tag < 600 )
1762 # oversize field (textarea)
1763 $subfield_data{marc_value} = qq(<textarea id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength">$defaultvalue</textarea>\n");
1764 } else {
1765 $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1767 push( @loop_data, \%subfield_data );
1771 my $itemnumber;
1772 if ( $itemrecord && $itemrecord->field($itemtagfield) ) {
1773 $itemnumber = $itemrecord->subfield( $itemtagfield, $itemtagsubfield );
1775 return {
1776 'itemtagfield' => $itemtagfield,
1777 'itemtagsubfield' => $itemtagsubfield,
1778 'itemnumber' => $itemnumber,
1779 'iteminformation' => \@loop_data
1783 sub ToggleNewStatus {
1784 my ( $params ) = @_;
1785 my @rules = @{ $params->{rules} };
1786 my $report_only = $params->{report_only};
1788 my $dbh = C4::Context->dbh;
1789 my @errors;
1790 my @item_columns = map { "items.$_" } Koha::Items->columns;
1791 my @biblioitem_columns = map { "biblioitems.$_" } Koha::Biblioitems->columns;
1792 my $report;
1793 for my $rule ( @rules ) {
1794 my $age = $rule->{age};
1795 my $conditions = $rule->{conditions};
1796 my $substitutions = $rule->{substitutions};
1797 foreach ( @$substitutions ) {
1798 ( $_->{item_field} ) = ( $_->{field} =~ /items\.(.*)/ );
1800 my @params;
1802 my $query = q|
1803 SELECT items.*
1804 FROM items
1805 LEFT JOIN biblioitems ON biblioitems.biblionumber = items.biblionumber
1806 WHERE 1
1808 for my $condition ( @$conditions ) {
1809 if (
1810 grep { $_ eq $condition->{field} } @item_columns
1811 or grep { $_ eq $condition->{field} } @biblioitem_columns
1813 if ( $condition->{value} =~ /\|/ ) {
1814 my @values = split /\|/, $condition->{value};
1815 $query .= qq| AND $condition->{field} IN (|
1816 . join( ',', ('?') x scalar @values )
1817 . q|)|;
1818 push @params, @values;
1819 } else {
1820 $query .= qq| AND $condition->{field} = ?|;
1821 push @params, $condition->{value};
1825 if ( defined $age ) {
1826 $query .= q| AND TO_DAYS(NOW()) - TO_DAYS(dateaccessioned) >= ? |;
1827 push @params, $age;
1829 my $sth = $dbh->prepare($query);
1830 $sth->execute( @params );
1831 while ( my $values = $sth->fetchrow_hashref ) {
1832 my $biblionumber = $values->{biblionumber};
1833 my $itemnumber = $values->{itemnumber};
1834 my $item = Koha::Items->find($itemnumber);
1835 for my $substitution ( @$substitutions ) {
1836 my $field = $substitution->{item_field};
1837 my $value = $substitution->{value};
1838 next unless $substitution->{field};
1839 next if ( defined $values->{ $substitution->{item_field} } and $values->{ $substitution->{item_field} } eq $substitution->{value} );
1840 $item->$field($value);
1841 push @{ $report->{$itemnumber} }, $substitution;
1843 $item->store unless $report_only;
1847 return $report;