Bug 24610: Let user switch between 'Pay' and 'Write off' mode
[koha.git] / C4 / Items.pm
blobf2db7a4894ed04399478c91229e6560a48759894
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 => 1|0
154 =cut
156 sub AddItemFromMarc {
157 my $source_item_marc = shift;
158 my $biblionumber = shift;
159 my $params = @_ ? shift : {};
161 my $dbh = C4::Context->dbh;
163 # parse item hash from MARC
164 my $frameworkcode = C4::Biblio::GetFrameworkCode($biblionumber);
165 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
167 my $localitemmarc = MARC::Record->new;
168 $localitemmarc->append_fields( $source_item_marc->field($itemtag) );
170 my $item_values = C4::Biblio::TransformMarcToKoha( $localitemmarc, $frameworkcode, 'items' );
171 my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
172 $item_values->{more_subfields_xml} = _get_unlinked_subfields_xml($unlinked_item_subfields);
173 $item_values->{biblionumber} = $biblionumber;
174 $item_values->{cn_source} = delete $item_values->{'items.cn_source'}; # Because of C4::Biblio::_disambiguate
175 $item_values->{cn_sort} = delete $item_values->{'items.cn_sort'}; # Because of C4::Biblio::_disambiguate
176 my $item = Koha::Item->new( $item_values )->store({ skip_modzebra_update => $params->{skip_modzebra_update} });
177 return ( $item->biblionumber, $item->biblioitemnumber, $item->itemnumber );
180 =head2 AddItemBatchFromMarc
182 ($itemnumber_ref, $error_ref) = AddItemBatchFromMarc($record,
183 $biblionumber, $biblioitemnumber, $frameworkcode);
185 Efficiently create item records from a MARC biblio record with
186 embedded item fields. This routine is suitable for batch jobs.
188 This API assumes that the bib record has already been
189 saved to the C<biblio> and C<biblioitems> tables. It does
190 not expect that C<biblio_metadata.metadata> is populated, but it
191 will do so via a call to ModBibiloMarc.
193 The goal of this API is to have a similar effect to using AddBiblio
194 and AddItems in succession, but without inefficient repeated
195 parsing of the MARC XML bib record.
197 This function returns an arrayref of new itemsnumbers and an arrayref of item
198 errors encountered during the processing. Each entry in the errors
199 list is a hashref containing the following keys:
201 =over
203 =item item_sequence
205 Sequence number of original item tag in the MARC record.
207 =item item_barcode
209 Item barcode, provide to assist in the construction of
210 useful error messages.
212 =item error_code
214 Code representing the error condition. Can be 'duplicate_barcode',
215 'invalid_homebranch', or 'invalid_holdingbranch'.
217 =item error_information
219 Additional information appropriate to the error condition.
221 =back
223 =cut
225 sub AddItemBatchFromMarc {
226 my ($record, $biblionumber, $biblioitemnumber, $frameworkcode) = @_;
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' );
297 # Retrieving the values for the fields that are not linked
298 my @mapped_fields = Koha::MarcSubfieldStructures->search(
300 frameworkcode => $frameworkcode,
301 kohafield => { -like => "items.%" }
303 )->get_column('kohafield');
304 for my $c ( $item_object->_result->result_source->columns ) {
305 next if grep { "items.$c" eq $_ } @mapped_fields;
306 $item->{$c} = $item_object->$c;
309 $item->{cn_source} = delete $item->{'items.cn_source'}; # Because of C4::Biblio::_disambiguate
310 $item->{cn_sort} = delete $item->{'items.cn_sort'}; # Because of C4::Biblio::_disambiguate
311 $item->{itemnumber} = $itemnumber;
312 $item->{biblionumber} = $biblionumber;
313 $item_object = $item_object->set_or_blank($item);
314 my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
315 $item_object->more_subfields_xml(_get_unlinked_subfields_xml($unlinked_item_subfields));
316 $item_object->store;
318 return $item_object->unblessed;
321 =head2 ModItemTransfer
323 ModItemTransfer($itemnumber, $frombranch, $tobranch, $trigger);
325 Marks an item as being transferred from one branch to another and records the trigger.
327 =cut
329 sub ModItemTransfer {
330 my ( $itemnumber, $frombranch, $tobranch, $trigger ) = @_;
332 my $dbh = C4::Context->dbh;
333 my $item = Koha::Items->find( $itemnumber );
335 # Remove the 'shelving cart' location status if it is being used.
336 CartToShelf( $itemnumber ) if $item->location && $item->location eq 'CART' && ( !$item->permanent_location || $item->permanent_location ne 'CART' );
338 $dbh->do("UPDATE branchtransfers SET datearrived = NOW(), comments = ? WHERE itemnumber = ? AND datearrived IS NULL", undef, "Canceled, new transfer from $frombranch to $tobranch created", $itemnumber);
340 #new entry in branchtransfers....
341 my $sth = $dbh->prepare(
342 "INSERT INTO branchtransfers (itemnumber, frombranch, datesent, tobranch, reason)
343 VALUES (?, ?, NOW(), ?, ?)");
344 $sth->execute($itemnumber, $frombranch, $tobranch, $trigger);
346 # FIXME we are fetching the item twice in the 2 next statements!
347 Koha::Items->find($itemnumber)->holdingbranch($frombranch)->store({ log_action => 0 });
348 ModDateLastSeen($itemnumber);
349 return;
352 =head2 ModDateLastSeen
354 ModDateLastSeen( $itemnumber, $leave_item_lost );
356 Mark item as seen. Is called when an item is issued, returned or manually marked during inventory/stocktaking.
357 C<$itemnumber> is the item number
358 C<$leave_item_lost> determines if a lost item will be found or remain lost
360 =cut
362 sub ModDateLastSeen {
363 my ( $itemnumber, $leave_item_lost ) = @_;
365 my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
367 my $item = Koha::Items->find($itemnumber);
368 $item->datelastseen($today);
369 $item->itemlost(0) unless $leave_item_lost;
370 $item->store({ log_action => 0 });
373 =head2 CheckItemPreSave
375 my $item_ref = TransformMarcToKoha($marc, 'items');
376 # do stuff
377 my %errors = CheckItemPreSave($item_ref);
378 if (exists $errors{'duplicate_barcode'}) {
379 print "item has duplicate barcode: ", $errors{'duplicate_barcode'}, "\n";
380 } elsif (exists $errors{'invalid_homebranch'}) {
381 print "item has invalid home branch: ", $errors{'invalid_homebranch'}, "\n";
382 } elsif (exists $errors{'invalid_holdingbranch'}) {
383 print "item has invalid holding branch: ", $errors{'invalid_holdingbranch'}, "\n";
384 } else {
385 print "item is OK";
388 Given a hashref containing item fields, determine if it can be
389 inserted or updated in the database. Specifically, checks for
390 database integrity issues, and returns a hash containing any
391 of the following keys, if applicable.
393 =over 2
395 =item duplicate_barcode
397 Barcode, if it duplicates one already found in the database.
399 =item invalid_homebranch
401 Home branch, if not defined in branches table.
403 =item invalid_holdingbranch
405 Holding branch, if not defined in branches table.
407 =back
409 This function does NOT implement any policy-related checks,
410 e.g., whether current operator is allowed to save an
411 item that has a given branch code.
413 =cut
415 sub CheckItemPreSave {
416 my $item_ref = shift;
418 my %errors = ();
420 # check for duplicate barcode
421 if (exists $item_ref->{'barcode'} and defined $item_ref->{'barcode'}) {
422 my $existing_item= Koha::Items->find({barcode => $item_ref->{'barcode'}});
423 if ($existing_item) {
424 if (!exists $item_ref->{'itemnumber'} # new item
425 or $item_ref->{'itemnumber'} != $existing_item->itemnumber) { # existing item
426 $errors{'duplicate_barcode'} = $item_ref->{'barcode'};
431 # check for valid home branch
432 if (exists $item_ref->{'homebranch'} and defined $item_ref->{'homebranch'}) {
433 my $home_library = Koha::Libraries->find( $item_ref->{homebranch} );
434 unless (defined $home_library) {
435 $errors{'invalid_homebranch'} = $item_ref->{'homebranch'};
439 # check for valid holding branch
440 if (exists $item_ref->{'holdingbranch'} and defined $item_ref->{'holdingbranch'}) {
441 my $holding_library = Koha::Libraries->find( $item_ref->{holdingbranch} );
442 unless (defined $holding_library) {
443 $errors{'invalid_holdingbranch'} = $item_ref->{'holdingbranch'};
447 return %errors;
451 =head1 EXPORTED SPECIAL ACCESSOR FUNCTIONS
453 The following functions provide various ways of
454 getting an item record, a set of item records, or
455 lists of authorized values for certain item fields.
457 =cut
459 =head2 GetItemsForInventory
461 ($itemlist, $iTotalRecords) = GetItemsForInventory( {
462 minlocation => $minlocation,
463 maxlocation => $maxlocation,
464 location => $location,
465 itemtype => $itemtype,
466 ignoreissued => $ignoreissued,
467 datelastseen => $datelastseen,
468 branchcode => $branchcode,
469 branch => $branch,
470 offset => $offset,
471 size => $size,
472 statushash => $statushash,
473 } );
475 Retrieve a list of title/authors/barcode/callnumber, for biblio inventory.
477 The sub returns a reference to a list of hashes, each containing
478 itemnumber, author, title, barcode, item callnumber, and date last
479 seen. It is ordered by callnumber then title.
481 The required minlocation & maxlocation parameters are used to specify a range of item callnumbers
482 the datelastseen can be used to specify that you want to see items not seen since a past date only.
483 offset & size can be used to retrieve only a part of the whole listing (defaut behaviour)
484 $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.
486 $iTotalRecords is the number of rows that would have been returned without the $offset, $size limit clause
488 =cut
490 sub GetItemsForInventory {
491 my ( $parameters ) = @_;
492 my $minlocation = $parameters->{'minlocation'} // '';
493 my $maxlocation = $parameters->{'maxlocation'} // '';
494 my $class_source = $parameters->{'class_source'} // C4::Context->preference('DefaultClassificationSource');
495 my $location = $parameters->{'location'} // '';
496 my $itemtype = $parameters->{'itemtype'} // '';
497 my $ignoreissued = $parameters->{'ignoreissued'} // '';
498 my $datelastseen = $parameters->{'datelastseen'} // '';
499 my $branchcode = $parameters->{'branchcode'} // '';
500 my $branch = $parameters->{'branch'} // '';
501 my $offset = $parameters->{'offset'} // '';
502 my $size = $parameters->{'size'} // '';
503 my $statushash = $parameters->{'statushash'} // '';
504 my $ignore_waiting_holds = $parameters->{'ignore_waiting_holds'} // '';
506 my $dbh = C4::Context->dbh;
507 my ( @bind_params, @where_strings );
509 my $min_cnsort = GetClassSort($class_source,undef,$minlocation);
510 my $max_cnsort = GetClassSort($class_source,undef,$maxlocation);
512 my $select_columns = q{
513 SELECT DISTINCT(items.itemnumber), barcode, itemcallnumber, title, author, biblio.biblionumber, biblio.frameworkcode, datelastseen, homebranch, location, notforloan, damaged, itemlost, withdrawn, stocknumber, items.cn_sort
515 my $select_count = q{SELECT COUNT(DISTINCT(items.itemnumber))};
516 my $query = q{
517 FROM items
518 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
519 LEFT JOIN biblioitems on items.biblionumber = biblioitems.biblionumber
521 if ($statushash){
522 for my $authvfield (keys %$statushash){
523 if ( scalar @{$statushash->{$authvfield}} > 0 ){
524 my $joinedvals = join ',', @{$statushash->{$authvfield}};
525 push @where_strings, "$authvfield in (" . $joinedvals . ")";
530 if ($minlocation) {
531 push @where_strings, 'items.cn_sort >= ?';
532 push @bind_params, $min_cnsort;
535 if ($maxlocation) {
536 push @where_strings, 'items.cn_sort <= ?';
537 push @bind_params, $max_cnsort;
540 if ($datelastseen) {
541 $datelastseen = output_pref({ str => $datelastseen, dateformat => 'iso', dateonly => 1 });
542 push @where_strings, '(datelastseen < ? OR datelastseen IS NULL)';
543 push @bind_params, $datelastseen;
546 if ( $location ) {
547 push @where_strings, 'items.location = ?';
548 push @bind_params, $location;
551 if ( $branchcode ) {
552 if($branch eq "homebranch"){
553 push @where_strings, 'items.homebranch = ?';
554 }else{
555 push @where_strings, 'items.holdingbranch = ?';
557 push @bind_params, $branchcode;
560 if ( $itemtype ) {
561 push @where_strings, 'biblioitems.itemtype = ?';
562 push @bind_params, $itemtype;
565 if ( $ignoreissued) {
566 $query .= "LEFT JOIN issues ON items.itemnumber = issues.itemnumber ";
567 push @where_strings, 'issues.date_due IS NULL';
570 if ( $ignore_waiting_holds ) {
571 $query .= "LEFT JOIN reserves ON items.itemnumber = reserves.itemnumber ";
572 push( @where_strings, q{(reserves.found != 'W' OR reserves.found IS NULL)} );
575 if ( @where_strings ) {
576 $query .= 'WHERE ';
577 $query .= join ' AND ', @where_strings;
579 my $count_query = $select_count . $query;
580 $query .= ' ORDER BY items.cn_sort, itemcallnumber, title';
581 $query .= " LIMIT $offset, $size" if ($offset and $size);
582 $query = $select_columns . $query;
583 my $sth = $dbh->prepare($query);
584 $sth->execute( @bind_params );
586 my @results = ();
587 my $tmpresults = $sth->fetchall_arrayref({});
588 $sth = $dbh->prepare( $count_query );
589 $sth->execute( @bind_params );
590 my ($iTotalRecords) = $sth->fetchrow_array();
592 my @avs = Koha::AuthorisedValues->search(
593 { 'marc_subfield_structures.kohafield' => { '>' => '' },
594 'me.authorised_value' => { '>' => '' },
596 { join => { category => 'marc_subfield_structures' },
597 distinct => ['marc_subfield_structures.kohafield, me.category, frameworkcode, me.authorised_value'],
598 '+select' => [ 'marc_subfield_structures.kohafield', 'marc_subfield_structures.frameworkcode', 'me.authorised_value', 'me.lib' ],
599 '+as' => [ 'kohafield', 'frameworkcode', 'authorised_value', 'lib' ],
603 my $avmapping = { map { $_->get_column('kohafield') . ',' . $_->get_column('frameworkcode') . ',' . $_->get_column('authorised_value') => $_->get_column('lib') } @avs };
605 foreach my $row (@$tmpresults) {
607 # Auth values
608 foreach (keys %$row) {
609 if (
610 defined(
611 $avmapping->{ "items.$_," . $row->{'frameworkcode'} . "," . ( $row->{$_} // q{} ) }
614 $row->{$_} = $avmapping->{"items.$_,".$row->{'frameworkcode'}.",".$row->{$_}};
617 push @results, $row;
620 return (\@results, $iTotalRecords);
623 =head2 GetItemsInfo
625 @results = GetItemsInfo($biblionumber);
627 Returns information about items with the given biblionumber.
629 C<GetItemsInfo> returns a list of references-to-hash. Each element
630 contains a number of keys. Most of them are attributes from the
631 C<biblio>, C<biblioitems>, C<items>, and C<itemtypes> tables in the
632 Koha database. Other keys include:
634 =over 2
636 =item C<$data-E<gt>{branchname}>
638 The name (not the code) of the branch to which the book belongs.
640 =item C<$data-E<gt>{datelastseen}>
642 This is simply C<items.datelastseen>, except that while the date is
643 stored in YYYY-MM-DD format in the database, here it is converted to
644 DD/MM/YYYY format. A NULL date is returned as C<//>.
646 =item C<$data-E<gt>{datedue}>
648 =item C<$data-E<gt>{class}>
650 This is the concatenation of C<biblioitems.classification>, the book's
651 Dewey code, and C<biblioitems.subclass>.
653 =item C<$data-E<gt>{ocount}>
655 I think this is the number of copies of the book available.
657 =item C<$data-E<gt>{order}>
659 If this is set, it is set to C<One Order>.
661 =back
663 =cut
665 sub GetItemsInfo {
666 my ( $biblionumber ) = @_;
667 my $dbh = C4::Context->dbh;
668 require C4::Languages;
669 my $language = C4::Languages::getlanguage();
670 my $query = "
671 SELECT items.*,
672 biblio.*,
673 biblioitems.volume,
674 biblioitems.number,
675 biblioitems.itemtype,
676 biblioitems.isbn,
677 biblioitems.issn,
678 biblioitems.publicationyear,
679 biblioitems.publishercode,
680 biblioitems.volumedate,
681 biblioitems.volumedesc,
682 biblioitems.lccn,
683 biblioitems.url,
684 items.notforloan as itemnotforloan,
685 issues.borrowernumber,
686 issues.date_due as datedue,
687 issues.onsite_checkout,
688 borrowers.cardnumber,
689 borrowers.surname,
690 borrowers.firstname,
691 borrowers.branchcode as bcode,
692 serial.serialseq,
693 serial.publisheddate,
694 itemtypes.description,
695 COALESCE( localization.translation, itemtypes.description ) AS translated_description,
696 itemtypes.notforloan as notforloan_per_itemtype,
697 holding.branchurl,
698 holding.branchcode,
699 holding.branchname,
700 holding.opac_info as holding_branch_opac_info,
701 home.opac_info as home_branch_opac_info,
702 IF(tmp_holdsqueue.itemnumber,1,0) AS has_pending_hold
703 FROM items
704 LEFT JOIN branches AS holding ON items.holdingbranch = holding.branchcode
705 LEFT JOIN branches AS home ON items.homebranch=home.branchcode
706 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
707 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
708 LEFT JOIN issues USING (itemnumber)
709 LEFT JOIN borrowers USING (borrowernumber)
710 LEFT JOIN serialitems USING (itemnumber)
711 LEFT JOIN serial USING (serialid)
712 LEFT JOIN itemtypes ON itemtypes.itemtype = "
713 . (C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype');
714 $query .= q|
715 LEFT JOIN tmp_holdsqueue USING (itemnumber)
716 LEFT JOIN localization ON itemtypes.itemtype = localization.code
717 AND localization.entity = 'itemtypes'
718 AND localization.lang = ?
721 $query .= " WHERE items.biblionumber = ? ORDER BY home.branchname, items.enumchron, LPAD( items.copynumber, 8, '0' ), items.dateaccessioned DESC" ;
722 my $sth = $dbh->prepare($query);
723 $sth->execute($language, $biblionumber);
724 my $i = 0;
725 my @results;
726 my $serial;
728 my $userenv = C4::Context->userenv;
729 my $want_not_same_branch = C4::Context->preference("IndependentBranches") && !C4::Context->IsSuperLibrarian();
730 while ( my $data = $sth->fetchrow_hashref ) {
731 if ( $data->{borrowernumber} && $want_not_same_branch) {
732 $data->{'NOTSAMEBRANCH'} = $data->{'bcode'} ne $userenv->{branch};
735 $serial ||= $data->{'serial'};
737 my $descriptions;
738 # get notforloan complete status if applicable
739 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.notforloan', authorised_value => $data->{itemnotforloan} });
740 $data->{notforloanvalue} = $descriptions->{lib} // '';
741 $data->{notforloanvalueopac} = $descriptions->{opac_description} // '';
743 # get restricted status and description if applicable
744 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.restricted', authorised_value => $data->{restricted} });
745 $data->{restrictedvalue} = $descriptions->{lib} // '';
746 $data->{restrictedvalueopac} = $descriptions->{opac_description} // '';
748 # my stack procedures
749 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.stack', authorised_value => $data->{stack} });
750 $data->{stack} = $descriptions->{lib} // '';
752 # Find the last 3 people who borrowed this item.
753 my $sth2 = $dbh->prepare("SELECT * FROM old_issues,borrowers
754 WHERE itemnumber = ?
755 AND old_issues.borrowernumber = borrowers.borrowernumber
756 ORDER BY returndate DESC
757 LIMIT 3");
758 $sth2->execute($data->{'itemnumber'});
759 my $ii = 0;
760 while (my $data2 = $sth2->fetchrow_hashref()) {
761 $data->{"timestamp$ii"} = $data2->{'timestamp'} if $data2->{'timestamp'};
762 $data->{"card$ii"} = $data2->{'cardnumber'} if $data2->{'cardnumber'};
763 $data->{"borrower$ii"} = $data2->{'borrowernumber'} if $data2->{'borrowernumber'};
764 $ii++;
767 $results[$i] = $data;
768 $i++;
771 return $serial
772 ? sort { ($b->{'publisheddate'} || $b->{'enumchron'}) cmp ($a->{'publisheddate'} || $a->{'enumchron'}) } @results
773 : @results;
776 =head2 GetItemsLocationInfo
778 my @itemlocinfo = GetItemsLocationInfo($biblionumber);
780 Returns the branch names, shelving location and itemcallnumber for each item attached to the biblio in question
782 C<GetItemsInfo> returns a list of references-to-hash. Data returned:
784 =over 2
786 =item C<$data-E<gt>{homebranch}>
788 Branch Name of the item's homebranch
790 =item C<$data-E<gt>{holdingbranch}>
792 Branch Name of the item's holdingbranch
794 =item C<$data-E<gt>{location}>
796 Item's shelving location code
798 =item C<$data-E<gt>{location_intranet}>
800 The intranet description for the Shelving Location as set in authorised_values 'LOC'
802 =item C<$data-E<gt>{location_opac}>
804 The OPAC description for the Shelving Location as set in authorised_values 'LOC'. Falls back to intranet description if no OPAC
805 description is set.
807 =item C<$data-E<gt>{itemcallnumber}>
809 Item's itemcallnumber
811 =item C<$data-E<gt>{cn_sort}>
813 Item's call number normalized for sorting
815 =back
817 =cut
819 sub GetItemsLocationInfo {
820 my $biblionumber = shift;
821 my @results;
823 my $dbh = C4::Context->dbh;
824 my $query = "SELECT a.branchname as homebranch, b.branchname as holdingbranch,
825 location, itemcallnumber, cn_sort
826 FROM items, branches as a, branches as b
827 WHERE homebranch = a.branchcode AND holdingbranch = b.branchcode
828 AND biblionumber = ?
829 ORDER BY cn_sort ASC";
830 my $sth = $dbh->prepare($query);
831 $sth->execute($biblionumber);
833 while ( my $data = $sth->fetchrow_hashref ) {
834 my $av = Koha::AuthorisedValues->search({ category => 'LOC', authorised_value => $data->{location} });
835 $av = $av->count ? $av->next : undef;
836 $data->{location_intranet} = $av ? $av->lib : '';
837 $data->{location_opac} = $av ? $av->opac_description : '';
838 push @results, $data;
840 return @results;
843 =head2 GetHostItemsInfo
845 $hostiteminfo = GetHostItemsInfo($hostfield);
846 Returns the iteminfo for items linked to records via a host field
848 =cut
850 sub GetHostItemsInfo {
851 my ($record) = @_;
852 my @returnitemsInfo;
854 if( !C4::Context->preference('EasyAnalyticalRecords') ) {
855 return @returnitemsInfo;
858 my @fields;
859 if( C4::Context->preference('marcflavour') eq 'MARC21' ||
860 C4::Context->preference('marcflavour') eq 'NORMARC') {
861 @fields = $record->field('773');
862 } elsif( C4::Context->preference('marcflavour') eq 'UNIMARC') {
863 @fields = $record->field('461');
866 foreach my $hostfield ( @fields ) {
867 my $hostbiblionumber = $hostfield->subfield("0");
868 my $linkeditemnumber = $hostfield->subfield("9");
869 my @hostitemInfos = GetItemsInfo($hostbiblionumber);
870 foreach my $hostitemInfo (@hostitemInfos) {
871 if( $hostitemInfo->{itemnumber} eq $linkeditemnumber ) {
872 push @returnitemsInfo, $hostitemInfo;
873 last;
877 return @returnitemsInfo;
880 =head2 get_hostitemnumbers_of
882 my @itemnumbers_of = get_hostitemnumbers_of($biblionumber);
884 Given a biblionumber, return the list of corresponding itemnumbers that are linked to it via host fields
886 Return a reference on a hash where key is a biblionumber and values are
887 references on array of itemnumbers.
889 =cut
892 sub get_hostitemnumbers_of {
893 my ($biblionumber) = @_;
895 if( !C4::Context->preference('EasyAnalyticalRecords') ) {
896 return ();
899 my $marcrecord = C4::Biblio::GetMarcBiblio({ biblionumber => $biblionumber });
900 return unless $marcrecord;
902 my ( @returnhostitemnumbers, $tag, $biblio_s, $item_s );
904 my $marcflavor = C4::Context->preference('marcflavour');
905 if ( $marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC' ) {
906 $tag = '773';
907 $biblio_s = '0';
908 $item_s = '9';
910 elsif ( $marcflavor eq 'UNIMARC' ) {
911 $tag = '461';
912 $biblio_s = '0';
913 $item_s = '9';
916 foreach my $hostfield ( $marcrecord->field($tag) ) {
917 my $hostbiblionumber = $hostfield->subfield($biblio_s);
918 next unless $hostbiblionumber; # have tag, don't have $biblio_s subfield
919 my $linkeditemnumber = $hostfield->subfield($item_s);
920 if ( ! $linkeditemnumber ) {
921 warn "ERROR biblionumber $biblionumber has 773^0, but doesn't have 9";
922 next;
924 my $is_from_biblio = Koha::Items->search({ itemnumber => $linkeditemnumber, biblionumber => $hostbiblionumber });
925 push @returnhostitemnumbers, $linkeditemnumber
926 if $is_from_biblio;
929 return @returnhostitemnumbers;
932 =head2 GetHiddenItemnumbers
934 my @itemnumbers_to_hide = GetHiddenItemnumbers({ items => \@items, borcat => $category });
936 Given a list of items it checks which should be hidden from the OPAC given
937 the current configuration. Returns a list of itemnumbers corresponding to
938 those that should be hidden. Optionally takes a borcat parameter for certain borrower types
939 to be excluded
941 =cut
943 sub GetHiddenItemnumbers {
944 my $params = shift;
945 my $items = $params->{items};
946 if (my $exceptions = C4::Context->preference('OpacHiddenItemsExceptions') and $params->{'borcat'}){
947 foreach my $except (split(/\|/, $exceptions)){
948 if ($params->{'borcat'} eq $except){
949 return; # we don't hide anything for this borrower category
953 my @resultitems;
955 my $yaml = C4::Context->preference('OpacHiddenItems');
956 return () if (! $yaml =~ /\S/ );
957 $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
958 my $hidingrules;
959 eval {
960 $hidingrules = YAML::Load($yaml);
962 if ($@) {
963 warn "Unable to parse OpacHiddenItems syspref : $@";
964 return ();
966 my $dbh = C4::Context->dbh;
968 # For each item
969 foreach my $item (@$items) {
971 # We check each rule
972 foreach my $field (keys %$hidingrules) {
973 my $val;
974 if (exists $item->{$field}) {
975 $val = $item->{$field};
977 else {
978 my $query = "SELECT $field from items where itemnumber = ?";
979 $val = $dbh->selectrow_array($query, undef, $item->{'itemnumber'});
981 $val = '' unless defined $val;
983 # If the results matches the values in the yaml file
984 if (any { $val eq $_ } @{$hidingrules->{$field}}) {
986 # We add the itemnumber to the list
987 push @resultitems, $item->{'itemnumber'};
989 # If at least one rule matched for an item, no need to test the others
990 last;
994 return @resultitems;
997 =head1 LIMITED USE FUNCTIONS
999 The following functions, while part of the public API,
1000 are not exported. This is generally because they are
1001 meant to be used by only one script for a specific
1002 purpose, and should not be used in any other context
1003 without careful thought.
1005 =cut
1007 =head2 GetMarcItem
1009 my $item_marc = GetMarcItem($biblionumber, $itemnumber);
1011 Returns MARC::Record of the item passed in parameter.
1012 This function is meant for use only in C<cataloguing/additem.pl>,
1013 where it is needed to support that script's MARC-like
1014 editor.
1016 =cut
1018 sub GetMarcItem {
1019 my ( $biblionumber, $itemnumber ) = @_;
1021 # GetMarcItem has been revised so that it does the following:
1022 # 1. Gets the item information from the items table.
1023 # 2. Converts it to a MARC field for storage in the bib record.
1025 # The previous behavior was:
1026 # 1. Get the bib record.
1027 # 2. Return the MARC tag corresponding to the item record.
1029 # The difference is that one treats the items row as authoritative,
1030 # while the other treats the MARC representation as authoritative
1031 # under certain circumstances.
1033 my $item = Koha::Items->find($itemnumber) or return;
1035 # Tack on 'items.' prefix to column names so that C4::Biblio::TransformKohaToMarc will work.
1036 # Also, don't emit a subfield if the underlying field is blank.
1038 return Item2Marc($item->unblessed, $biblionumber);
1041 sub Item2Marc {
1042 my ($itemrecord,$biblionumber)=@_;
1043 my $mungeditem = {
1044 map {
1045 defined($itemrecord->{$_}) && $itemrecord->{$_} ne '' ? ("items.$_" => $itemrecord->{$_}) : ()
1046 } keys %{ $itemrecord }
1048 my $framework = C4::Biblio::GetFrameworkCode( $biblionumber );
1049 my $itemmarc = C4::Biblio::TransformKohaToMarc( $mungeditem, { framework => $framework } );
1050 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField(
1051 "items.itemnumber", $framework,
1054 my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($mungeditem->{'items.more_subfields_xml'});
1055 if (defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1) {
1056 foreach my $field ($itemmarc->field($itemtag)){
1057 $field->add_subfields(@$unlinked_item_subfields);
1060 return $itemmarc;
1063 =head1 PRIVATE FUNCTIONS AND VARIABLES
1065 The following functions are not meant to be called
1066 directly, but are documented in order to explain
1067 the inner workings of C<C4::Items>.
1069 =cut
1071 =head2 MoveItemFromBiblio
1073 MoveItemFromBiblio($itenumber, $frombiblio, $tobiblio);
1075 Moves an item from a biblio to another
1077 Returns undef if the move failed or the biblionumber of the destination record otherwise
1079 =cut
1081 sub MoveItemFromBiblio {
1082 my ($itemnumber, $frombiblio, $tobiblio) = @_;
1083 my $dbh = C4::Context->dbh;
1084 my ( $tobiblioitem ) = $dbh->selectrow_array(q|
1085 SELECT biblioitemnumber
1086 FROM biblioitems
1087 WHERE biblionumber = ?
1088 |, undef, $tobiblio );
1089 my $return = $dbh->do(q|
1090 UPDATE items
1091 SET biblioitemnumber = ?,
1092 biblionumber = ?
1093 WHERE itemnumber = ?
1094 AND biblionumber = ?
1095 |, undef, $tobiblioitem, $tobiblio, $itemnumber, $frombiblio );
1096 if ($return == 1) {
1097 ModZebra( $tobiblio, "specialUpdate", "biblioserver" );
1098 ModZebra( $frombiblio, "specialUpdate", "biblioserver" );
1099 # Checking if the item we want to move is in an order
1100 require C4::Acquisition;
1101 my $order = C4::Acquisition::GetOrderFromItemnumber($itemnumber);
1102 if ($order) {
1103 # Replacing the biblionumber within the order if necessary
1104 $order->{'biblionumber'} = $tobiblio;
1105 C4::Acquisition::ModOrder($order);
1108 # Update reserves, hold_fill_targets, tmp_holdsqueue and linktracker tables
1109 for my $table_name ( qw( reserves hold_fill_targets tmp_holdsqueue linktracker ) ) {
1110 $dbh->do( qq|
1111 UPDATE $table_name
1112 SET biblionumber = ?
1113 WHERE itemnumber = ?
1114 |, undef, $tobiblio, $itemnumber );
1116 return $tobiblio;
1118 return;
1121 =head2 _marc_from_item_hash
1123 my $item_marc = _marc_from_item_hash($item, $frameworkcode[, $unlinked_item_subfields]);
1125 Given an item hash representing a complete item record,
1126 create a C<MARC::Record> object containing an embedded
1127 tag representing that item.
1129 The third, optional parameter C<$unlinked_item_subfields> is
1130 an arrayref of subfields (not mapped to C<items> fields per the
1131 framework) to be added to the MARC representation
1132 of the item.
1134 =cut
1136 sub _marc_from_item_hash {
1137 my $item = shift;
1138 my $frameworkcode = shift;
1139 my $unlinked_item_subfields;
1140 if (@_) {
1141 $unlinked_item_subfields = shift;
1144 # Tack on 'items.' prefix to column names so lookup from MARC frameworks will work
1145 # Also, don't emit a subfield if the underlying field is blank.
1146 my $mungeditem = { map { (defined($item->{$_}) and $item->{$_} ne '') ?
1147 (/^items\./ ? ($_ => $item->{$_}) : ("items.$_" => $item->{$_}))
1148 : () } keys %{ $item } };
1150 my $item_marc = MARC::Record->new();
1151 foreach my $item_field ( keys %{$mungeditem} ) {
1152 my ( $tag, $subfield ) = C4::Biblio::GetMarcFromKohaField( $item_field );
1153 next unless defined $tag and defined $subfield; # skip if not mapped to MARC field
1154 my @values = split(/\s?\|\s?/, $mungeditem->{$item_field}, -1);
1155 foreach my $value (@values){
1156 if ( my $field = $item_marc->field($tag) ) {
1157 $field->add_subfields( $subfield => $value );
1158 } else {
1159 my $add_subfields = [];
1160 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
1161 $add_subfields = $unlinked_item_subfields;
1163 $item_marc->add_fields( $tag, " ", " ", $subfield => $value, @$add_subfields );
1168 return $item_marc;
1171 =head2 _repack_item_errors
1173 Add an error message hash generated by C<CheckItemPreSave>
1174 to a list of errors.
1176 =cut
1178 sub _repack_item_errors {
1179 my $item_sequence_num = shift;
1180 my $item_ref = shift;
1181 my $error_ref = shift;
1183 my @repacked_errors = ();
1185 foreach my $error_code (sort keys %{ $error_ref }) {
1186 my $repacked_error = {};
1187 $repacked_error->{'item_sequence'} = $item_sequence_num;
1188 $repacked_error->{'item_barcode'} = exists($item_ref->{'barcode'}) ? $item_ref->{'barcode'} : '';
1189 $repacked_error->{'error_code'} = $error_code;
1190 $repacked_error->{'error_information'} = $error_ref->{$error_code};
1191 push @repacked_errors, $repacked_error;
1194 return @repacked_errors;
1197 =head2 _get_unlinked_item_subfields
1199 my $unlinked_item_subfields = _get_unlinked_item_subfields($original_item_marc, $frameworkcode);
1201 =cut
1203 sub _get_unlinked_item_subfields {
1204 my $original_item_marc = shift;
1205 my $frameworkcode = shift;
1207 my $marcstructure = C4::Biblio::GetMarcStructure(1, $frameworkcode, { unsafe => 1 });
1209 # assume that this record has only one field, and that that
1210 # field contains only the item information
1211 my $subfields = [];
1212 my @fields = $original_item_marc->fields();
1213 if ($#fields > -1) {
1214 my $field = $fields[0];
1215 my $tag = $field->tag();
1216 foreach my $subfield ($field->subfields()) {
1217 if (defined $subfield->[1] and
1218 $subfield->[1] ne '' and
1219 !$marcstructure->{$tag}->{$subfield->[0]}->{'kohafield'}) {
1220 push @$subfields, $subfield->[0] => $subfield->[1];
1224 return $subfields;
1227 =head2 _get_unlinked_subfields_xml
1229 my $unlinked_subfields_xml = _get_unlinked_subfields_xml($unlinked_item_subfields);
1231 =cut
1233 sub _get_unlinked_subfields_xml {
1234 my $unlinked_item_subfields = shift;
1236 my $xml;
1237 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
1238 my $marc = MARC::Record->new();
1239 # use of tag 999 is arbitrary, and doesn't need to match the item tag
1240 # used in the framework
1241 $marc->append_fields(MARC::Field->new('999', ' ', ' ', @$unlinked_item_subfields));
1242 $marc->encoding("UTF-8");
1243 $xml = $marc->as_xml("USMARC");
1246 return $xml;
1249 =head2 _parse_unlinked_item_subfields_from_xml
1251 my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($whole_item->{'more_subfields_xml'}):
1253 =cut
1255 sub _parse_unlinked_item_subfields_from_xml {
1256 my $xml = shift;
1257 require C4::Charset;
1258 return unless defined $xml and $xml ne "";
1259 my $marc = MARC::Record->new_from_xml(C4::Charset::StripNonXmlChars($xml),'UTF-8');
1260 my $unlinked_subfields = [];
1261 my @fields = $marc->fields();
1262 if ($#fields > -1) {
1263 foreach my $subfield ($fields[0]->subfields()) {
1264 push @$unlinked_subfields, $subfield->[0] => $subfield->[1];
1267 return $unlinked_subfields;
1270 =head2 GetAnalyticsCount
1272 $count= &GetAnalyticsCount($itemnumber)
1274 counts Usage of itemnumber in Analytical bibliorecords.
1276 =cut
1278 sub GetAnalyticsCount {
1279 my ($itemnumber) = @_;
1281 ### ZOOM search here
1282 my $query;
1283 $query= "hi=".$itemnumber;
1284 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
1285 my ($err,$res,$result) = $searcher->simple_search_compat($query,0,10);
1286 return ($result);
1289 sub _SearchItems_build_where_fragment {
1290 my ($filter) = @_;
1292 my $dbh = C4::Context->dbh;
1294 my $where_fragment;
1295 if (exists($filter->{conjunction})) {
1296 my (@where_strs, @where_args);
1297 foreach my $f (@{ $filter->{filters} }) {
1298 my $fragment = _SearchItems_build_where_fragment($f);
1299 if ($fragment) {
1300 push @where_strs, $fragment->{str};
1301 push @where_args, @{ $fragment->{args} };
1304 my $where_str = '';
1305 if (@where_strs) {
1306 $where_str = '(' . join (' ' . $filter->{conjunction} . ' ', @where_strs) . ')';
1307 $where_fragment = {
1308 str => $where_str,
1309 args => \@where_args,
1312 } else {
1313 my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1314 push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1315 push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1316 my @operators = qw(= != > < >= <= like);
1317 my $field = $filter->{field} // q{};
1318 if ( (0 < grep { $_ eq $field } @columns) or (substr($field, 0, 5) eq 'marc:') ) {
1319 my $op = $filter->{operator};
1320 my $query = $filter->{query};
1322 if (!$op or (0 == grep { $_ eq $op } @operators)) {
1323 $op = '='; # default operator
1326 my $column;
1327 if ($field =~ /^marc:(\d{3})(?:\$(\w))?$/) {
1328 my $marcfield = $1;
1329 my $marcsubfield = $2;
1330 my ($kohafield) = $dbh->selectrow_array(q|
1331 SELECT kohafield FROM marc_subfield_structure
1332 WHERE tagfield=? AND tagsubfield=? AND frameworkcode=''
1333 |, undef, $marcfield, $marcsubfield);
1335 if ($kohafield) {
1336 $column = $kohafield;
1337 } else {
1338 # MARC field is not linked to a DB field so we need to use
1339 # ExtractValue on marcxml from biblio_metadata or
1340 # items.more_subfields_xml, depending on the MARC field.
1341 my $xpath;
1342 my $sqlfield;
1343 my ($itemfield) = C4::Biblio::GetMarcFromKohaField('items.itemnumber');
1344 if ($marcfield eq $itemfield) {
1345 $sqlfield = 'more_subfields_xml';
1346 $xpath = '//record/datafield/subfield[@code="' . $marcsubfield . '"]';
1347 } else {
1348 $sqlfield = 'metadata'; # From biblio_metadata
1349 if ($marcfield < 10) {
1350 $xpath = "//record/controlfield[\@tag=\"$marcfield\"]";
1351 } else {
1352 $xpath = "//record/datafield[\@tag=\"$marcfield\"]/subfield[\@code=\"$marcsubfield\"]";
1355 $column = "ExtractValue($sqlfield, '$xpath')";
1357 } else {
1358 $column = $field;
1361 if (ref $query eq 'ARRAY') {
1362 if ($op eq '=') {
1363 $op = 'IN';
1364 } elsif ($op eq '!=') {
1365 $op = 'NOT IN';
1367 $where_fragment = {
1368 str => "$column $op (" . join (',', ('?') x @$query) . ")",
1369 args => $query,
1371 } else {
1372 $where_fragment = {
1373 str => "$column $op ?",
1374 args => [ $query ],
1380 return $where_fragment;
1383 =head2 SearchItems
1385 my ($items, $total) = SearchItems($filter, $params);
1387 Perform a search among items
1389 $filter is a reference to a hash which can be a filter, or a combination of filters.
1391 A filter has the following keys:
1393 =over 2
1395 =item * field: the name of a SQL column in table items
1397 =item * query: the value to search in this column
1399 =item * operator: comparison operator. Can be one of = != > < >= <= like
1401 =back
1403 A combination of filters hash the following keys:
1405 =over 2
1407 =item * conjunction: 'AND' or 'OR'
1409 =item * filters: array ref of filters
1411 =back
1413 $params is a reference to a hash that can contain the following parameters:
1415 =over 2
1417 =item * rows: Number of items to return. 0 returns everything (default: 0)
1419 =item * page: Page to return (return items from (page-1)*rows to (page*rows)-1)
1420 (default: 1)
1422 =item * sortby: A SQL column name in items table to sort on
1424 =item * sortorder: 'ASC' or 'DESC'
1426 =back
1428 =cut
1430 sub SearchItems {
1431 my ($filter, $params) = @_;
1433 $filter //= {};
1434 $params //= {};
1435 return unless ref $filter eq 'HASH';
1436 return unless ref $params eq 'HASH';
1438 # Default parameters
1439 $params->{rows} ||= 0;
1440 $params->{page} ||= 1;
1441 $params->{sortby} ||= 'itemnumber';
1442 $params->{sortorder} ||= 'ASC';
1444 my ($where_str, @where_args);
1445 my $where_fragment = _SearchItems_build_where_fragment($filter);
1446 if ($where_fragment) {
1447 $where_str = $where_fragment->{str};
1448 @where_args = @{ $where_fragment->{args} };
1451 my $dbh = C4::Context->dbh;
1452 my $query = q{
1453 SELECT SQL_CALC_FOUND_ROWS items.*
1454 FROM items
1455 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
1456 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
1457 LEFT JOIN biblio_metadata ON biblio_metadata.biblionumber = biblio.biblionumber
1458 WHERE 1
1460 if (defined $where_str and $where_str ne '') {
1461 $query .= qq{ AND $where_str };
1464 $query .= q{ AND biblio_metadata.format = 'marcxml' AND biblio_metadata.schema = ? };
1465 push @where_args, C4::Context->preference('marcflavour');
1467 my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1468 push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
1469 push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
1470 my $sortby = (0 < grep {$params->{sortby} eq $_} @columns)
1471 ? $params->{sortby} : 'itemnumber';
1472 my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
1473 $query .= qq{ ORDER BY $sortby $sortorder };
1475 my $rows = $params->{rows};
1476 my @limit_args;
1477 if ($rows > 0) {
1478 my $offset = $rows * ($params->{page}-1);
1479 $query .= qq { LIMIT ?, ? };
1480 push @limit_args, $offset, $rows;
1483 my $sth = $dbh->prepare($query);
1484 my $rv = $sth->execute(@where_args, @limit_args);
1486 return unless ($rv);
1487 my ($total_rows) = $dbh->selectrow_array(q{ SELECT FOUND_ROWS() });
1489 return ($sth->fetchall_arrayref({}), $total_rows);
1493 =head1 OTHER FUNCTIONS
1495 =head2 _find_value
1497 ($indicators, $value) = _find_value($tag, $subfield, $record,$encoding);
1499 Find the given $subfield in the given $tag in the given
1500 MARC::Record $record. If the subfield is found, returns
1501 the (indicators, value) pair; otherwise, (undef, undef) is
1502 returned.
1504 PROPOSITION :
1505 Such a function is used in addbiblio AND additem and serial-edit and maybe could be used in Authorities.
1506 I suggest we export it from this module.
1508 =cut
1510 sub _find_value {
1511 my ( $tagfield, $insubfield, $record, $encoding ) = @_;
1512 my @result;
1513 my $indicator;
1514 if ( $tagfield < 10 ) {
1515 if ( $record->field($tagfield) ) {
1516 push @result, $record->field($tagfield)->data();
1517 } else {
1518 push @result, "";
1520 } else {
1521 foreach my $field ( $record->field($tagfield) ) {
1522 my @subfields = $field->subfields();
1523 foreach my $subfield (@subfields) {
1524 if ( @$subfield[0] eq $insubfield ) {
1525 push @result, @$subfield[1];
1526 $indicator = $field->indicator(1) . $field->indicator(2);
1531 return ( $indicator, @result );
1535 =head2 PrepareItemrecordDisplay
1537 PrepareItemrecordDisplay($itemrecord,$bibnum,$itemumber,$frameworkcode);
1539 Returns a hash with all the fields for Display a given item data in a template
1541 The $frameworkcode returns the item for the given frameworkcode, ONLY if bibnum is not provided
1543 =cut
1545 sub PrepareItemrecordDisplay {
1547 my ( $bibnum, $itemnum, $defaultvalues, $frameworkcode ) = @_;
1549 my $dbh = C4::Context->dbh;
1550 $frameworkcode = C4::Biblio::GetFrameworkCode($bibnum) if $bibnum;
1551 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1553 # Note: $tagslib obtained from GetMarcStructure() in 'unsafe' mode is
1554 # a shared data structure. No plugin (including custom ones) should change
1555 # its contents. See also GetMarcStructure.
1556 my $tagslib = GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
1558 # return nothing if we don't have found an existing framework.
1559 return q{} unless $tagslib;
1560 my $itemrecord;
1561 if ($itemnum) {
1562 $itemrecord = C4::Items::GetMarcItem( $bibnum, $itemnum );
1564 my @loop_data;
1566 my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
1567 my $query = qq{
1568 SELECT authorised_value,lib FROM authorised_values
1570 $query .= qq{
1571 LEFT JOIN authorised_values_branches ON ( id = av_id )
1572 } if $branch_limit;
1573 $query .= qq{
1574 WHERE category = ?
1576 $query .= qq{ AND ( branchcode = ? OR branchcode IS NULL )} if $branch_limit;
1577 $query .= qq{ ORDER BY lib};
1578 my $authorised_values_sth = $dbh->prepare( $query );
1579 foreach my $tag ( sort keys %{$tagslib} ) {
1580 if ( $tag ne '' ) {
1582 # loop through each subfield
1583 my $cntsubf;
1584 foreach my $subfield ( sort keys %{ $tagslib->{$tag} } ) {
1585 next if IsMarcStructureInternal($tagslib->{$tag}{$subfield});
1586 next unless ( $tagslib->{$tag}->{$subfield}->{'tab'} );
1587 next if ( $tagslib->{$tag}->{$subfield}->{'tab'} ne "10" );
1588 my %subfield_data;
1589 $subfield_data{tag} = $tag;
1590 $subfield_data{subfield} = $subfield;
1591 $subfield_data{countsubfield} = $cntsubf++;
1592 $subfield_data{kohafield} = $tagslib->{$tag}->{$subfield}->{'kohafield'};
1593 $subfield_data{id} = "tag_".$tag."_subfield_".$subfield."_".int(rand(1000000));
1595 # $subfield_data{marc_lib}=$tagslib->{$tag}->{$subfield}->{lib};
1596 $subfield_data{marc_lib} = $tagslib->{$tag}->{$subfield}->{lib};
1597 $subfield_data{mandatory} = $tagslib->{$tag}->{$subfield}->{mandatory};
1598 $subfield_data{repeatable} = $tagslib->{$tag}->{$subfield}->{repeatable};
1599 $subfield_data{hidden} = "display:none"
1600 if ( ( $tagslib->{$tag}->{$subfield}->{hidden} > 4 )
1601 || ( $tagslib->{$tag}->{$subfield}->{hidden} < -4 ) );
1602 my ( $x, $defaultvalue );
1603 if ($itemrecord) {
1604 ( $x, $defaultvalue ) = _find_value( $tag, $subfield, $itemrecord );
1606 $defaultvalue = $tagslib->{$tag}->{$subfield}->{defaultvalue} unless $defaultvalue;
1607 if ( !defined $defaultvalue ) {
1608 $defaultvalue = q||;
1609 } else {
1610 $defaultvalue =~ s/"/&quot;/g;
1613 my $maxlength = $tagslib->{$tag}->{$subfield}->{maxlength};
1615 # search for itemcallnumber if applicable
1616 if ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
1617 && C4::Context->preference('itemcallnumber') && $itemrecord) {
1618 foreach my $itemcn_pref (split(/,/,C4::Context->preference('itemcallnumber'))){
1619 my $CNtag = substr( $itemcn_pref, 0, 3 );
1620 next unless my $field = $itemrecord->field($CNtag);
1621 my $CNsubfields = substr( $itemcn_pref, 3 );
1622 $defaultvalue = $field->as_string( $CNsubfields, ' ');
1623 last if $defaultvalue;
1626 if ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
1627 && $defaultvalues
1628 && $defaultvalues->{'callnumber'} ) {
1629 if( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ){
1630 # if the item record exists, only use default value if the item has no callnumber
1631 $defaultvalue = $defaultvalues->{callnumber};
1632 } elsif ( !$itemrecord and $defaultvalues ) {
1633 # if the item record *doesn't* exists, always use the default value
1634 $defaultvalue = $defaultvalues->{callnumber};
1637 if ( ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.holdingbranch' || $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.homebranch' )
1638 && $defaultvalues
1639 && $defaultvalues->{'branchcode'} ) {
1640 if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
1641 $defaultvalue = $defaultvalues->{branchcode};
1644 if ( ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.location' )
1645 && $defaultvalues
1646 && $defaultvalues->{'location'} ) {
1648 if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
1649 # if the item record exists, only use default value if the item has no locationr
1650 $defaultvalue = $defaultvalues->{location};
1651 } elsif ( !$itemrecord and $defaultvalues ) {
1652 # if the item record *doesn't* exists, always use the default value
1653 $defaultvalue = $defaultvalues->{location};
1656 if ( $tagslib->{$tag}->{$subfield}->{authorised_value} ) {
1657 my @authorised_values;
1658 my %authorised_lib;
1660 # builds list, depending on authorised value...
1661 #---- branch
1662 if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "branches" ) {
1663 if ( ( C4::Context->preference("IndependentBranches") )
1664 && !C4::Context->IsSuperLibrarian() ) {
1665 my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches WHERE branchcode = ? ORDER BY branchname" );
1666 $sth->execute( C4::Context->userenv->{branch} );
1667 push @authorised_values, ""
1668 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1669 while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1670 push @authorised_values, $branchcode;
1671 $authorised_lib{$branchcode} = $branchname;
1673 } else {
1674 my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches ORDER BY branchname" );
1675 $sth->execute;
1676 push @authorised_values, ""
1677 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
1678 while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
1679 push @authorised_values, $branchcode;
1680 $authorised_lib{$branchcode} = $branchname;
1684 $defaultvalue = C4::Context->userenv ? C4::Context->userenv->{branch} : undef;
1685 if ( $defaultvalues and $defaultvalues->{branchcode} ) {
1686 $defaultvalue = $defaultvalues->{branchcode};
1689 #----- itemtypes
1690 } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "itemtypes" ) {
1691 my $itemtypes = Koha::ItemTypes->search_with_localization;
1692 push @authorised_values, "";
1693 while ( my $itemtype = $itemtypes->next ) {
1694 push @authorised_values, $itemtype->itemtype;
1695 $authorised_lib{$itemtype->itemtype} = $itemtype->translated_description;
1697 if ($defaultvalues && $defaultvalues->{'itemtype'}) {
1698 $defaultvalue = $defaultvalues->{'itemtype'};
1701 #---- class_sources
1702 } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "cn_source" ) {
1703 push @authorised_values, "";
1705 my $class_sources = GetClassSources();
1706 my $default_source = $defaultvalue || C4::Context->preference("DefaultClassificationSource");
1708 foreach my $class_source (sort keys %$class_sources) {
1709 next unless $class_sources->{$class_source}->{'used'} or
1710 ($class_source eq $default_source);
1711 push @authorised_values, $class_source;
1712 $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
1715 $defaultvalue = $default_source;
1717 #---- "true" authorised value
1718 } else {
1719 $authorised_values_sth->execute(
1720 $tagslib->{$tag}->{$subfield}->{authorised_value},
1721 $branch_limit ? $branch_limit : ()
1723 push @authorised_values, "";
1724 while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
1725 push @authorised_values, $value;
1726 $authorised_lib{$value} = $lib;
1729 $subfield_data{marc_value} = {
1730 type => 'select',
1731 values => \@authorised_values,
1732 default => $defaultvalue // q{},
1733 labels => \%authorised_lib,
1735 } elsif ( $tagslib->{$tag}->{$subfield}->{value_builder} ) {
1736 # it is a plugin
1737 require Koha::FrameworkPlugin;
1738 my $plugin = Koha::FrameworkPlugin->new({
1739 name => $tagslib->{$tag}->{$subfield}->{value_builder},
1740 item_style => 1,
1742 my $pars = { dbh => $dbh, record => undef, tagslib =>$tagslib, id => $subfield_data{id}, tabloop => undef };
1743 $plugin->build( $pars );
1744 if ( $itemrecord and my $field = $itemrecord->field($tag) ) {
1745 $defaultvalue = $field->subfield($subfield) || q{};
1747 if( !$plugin->errstr ) {
1748 #TODO Move html to template; see report 12176/13397
1749 my $tab= $plugin->noclick? '-1': '';
1750 my $class= $plugin->noclick? ' disabled': '';
1751 my $title= $plugin->noclick? 'No popup': 'Tag editor';
1752 $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;
1753 } else {
1754 warn $plugin->errstr;
1755 $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
1758 elsif ( $tag eq '' ) { # it's an hidden field
1759 $subfield_data{marc_value} = qq(<input type="hidden" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1761 elsif ( $tagslib->{$tag}->{$subfield}->{'hidden'} ) { # FIXME: shouldn't input type be "hidden" ?
1762 $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1764 elsif ( length($defaultvalue) > 100
1765 or (C4::Context->preference("marcflavour") eq "UNIMARC" and
1766 300 <= $tag && $tag < 400 && $subfield eq 'a' )
1767 or (C4::Context->preference("marcflavour") eq "MARC21" and
1768 500 <= $tag && $tag < 600 )
1770 # oversize field (textarea)
1771 $subfield_data{marc_value} = qq(<textarea id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength">$defaultvalue</textarea>\n");
1772 } else {
1773 $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
1775 push( @loop_data, \%subfield_data );
1779 my $itemnumber;
1780 if ( $itemrecord && $itemrecord->field($itemtagfield) ) {
1781 $itemnumber = $itemrecord->subfield( $itemtagfield, $itemtagsubfield );
1783 return {
1784 'itemtagfield' => $itemtagfield,
1785 'itemtagsubfield' => $itemtagsubfield,
1786 'itemnumber' => $itemnumber,
1787 'iteminformation' => \@loop_data
1791 sub ToggleNewStatus {
1792 my ( $params ) = @_;
1793 my @rules = @{ $params->{rules} };
1794 my $report_only = $params->{report_only};
1796 my $dbh = C4::Context->dbh;
1797 my @errors;
1798 my @item_columns = map { "items.$_" } Koha::Items->columns;
1799 my @biblioitem_columns = map { "biblioitems.$_" } Koha::Biblioitems->columns;
1800 my $report;
1801 for my $rule ( @rules ) {
1802 my $age = $rule->{age};
1803 my $conditions = $rule->{conditions};
1804 my $substitutions = $rule->{substitutions};
1805 foreach ( @$substitutions ) {
1806 ( $_->{item_field} ) = ( $_->{field} =~ /items\.(.*)/ );
1808 my @params;
1810 my $query = q|
1811 SELECT items.*
1812 FROM items
1813 LEFT JOIN biblioitems ON biblioitems.biblionumber = items.biblionumber
1814 WHERE 1
1816 for my $condition ( @$conditions ) {
1817 if (
1818 grep { $_ eq $condition->{field} } @item_columns
1819 or grep { $_ eq $condition->{field} } @biblioitem_columns
1821 if ( $condition->{value} =~ /\|/ ) {
1822 my @values = split /\|/, $condition->{value};
1823 $query .= qq| AND $condition->{field} IN (|
1824 . join( ',', ('?') x scalar @values )
1825 . q|)|;
1826 push @params, @values;
1827 } else {
1828 $query .= qq| AND $condition->{field} = ?|;
1829 push @params, $condition->{value};
1833 if ( defined $age ) {
1834 $query .= q| AND TO_DAYS(NOW()) - TO_DAYS(dateaccessioned) >= ? |;
1835 push @params, $age;
1837 my $sth = $dbh->prepare($query);
1838 $sth->execute( @params );
1839 while ( my $values = $sth->fetchrow_hashref ) {
1840 my $biblionumber = $values->{biblionumber};
1841 my $itemnumber = $values->{itemnumber};
1842 my $item = Koha::Items->find($itemnumber);
1843 for my $substitution ( @$substitutions ) {
1844 my $field = $substitution->{item_field};
1845 my $value = $substitution->{value};
1846 next unless $substitution->{field};
1847 next if ( defined $values->{ $substitution->{item_field} } and $values->{ $substitution->{item_field} } eq $substitution->{value} );
1848 $item->$field($value);
1849 push @{ $report->{$itemnumber} }, $substitution;
1851 $item->store unless $report_only;
1855 return $report;