Bug 24774: Set JSON indentation of 2 spaces in .editorconfig
[koha.git] / C4 / Items.pm
blob8ed1c5efed6d87b8f70ecf6c5134f5676ee54b91
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 AddItem
31 AddItemBatchFromMarc
32 ModItemFromMarc
33 Item2Marc
34 ModItem
35 ModDateLastSeen
36 ModItemTransfer
37 DelItem
38 CheckItemPreSave
39 GetItemsForInventory
40 GetItemsInfo
41 GetItemsLocationInfo
42 GetHostItemsInfo
43 get_hostitemnumbers_of
44 GetHiddenItemnumbers
45 ItemSafeToDelete
46 DelItemCheck
47 MoveItemFromBiblio
48 CartToShelf
49 GetAnalyticsCount
50 SearchItemsByField
51 SearchItems
52 PrepareItemrecordDisplay
56 use Carp;
57 use Try::Tiny;
58 use C4::Context;
59 use C4::Koha;
60 use C4::Biblio;
61 use Koha::DateUtils;
62 use MARC::Record;
63 use C4::ClassSource;
64 use C4::Log;
65 use List::MoreUtils qw(any);
66 use YAML qw(Load);
67 use DateTime::Format::MySQL;
68 use Data::Dumper; # used as part of logging item record changes, not just for
69 # debugging; so please don't remove this
71 use Koha::AuthorisedValues;
72 use Koha::DateUtils qw(dt_from_string);
73 use Koha::Database;
75 use Koha::Biblioitems;
76 use Koha::Items;
77 use Koha::ItemTypes;
78 use Koha::SearchEngine;
79 use Koha::SearchEngine::Search;
80 use Koha::Libraries;
82 =head1 NAME
84 C4::Items - item management functions
86 =head1 DESCRIPTION
88 This module contains an API for manipulating item
89 records in Koha, and is used by cataloguing, circulation,
90 acquisitions, and serials management.
92 # FIXME This POD is not up-to-date
93 A Koha item record is stored in two places: the
94 items table and embedded in a MARC tag in the XML
95 version of the associated bib record in C<biblioitems.marcxml>.
96 This is done to allow the item information to be readily
97 indexed (e.g., by Zebra), but means that each item
98 modification transaction must keep the items table
99 and the MARC XML in sync at all times.
101 The items table will be considered authoritative. In other
102 words, if there is ever a discrepancy between the items
103 table and the MARC XML, the items table should be considered
104 accurate.
106 =head1 HISTORICAL NOTE
108 Most of the functions in C<C4::Items> were originally in
109 the C<C4::Biblio> module.
111 =head1 CORE EXPORTED FUNCTIONS
113 The following functions are meant for use by users
114 of C<C4::Items>
116 =cut
118 =head2 CartToShelf
120 CartToShelf($itemnumber);
122 Set the current shelving location of the item record
123 to its stored permanent shelving location. This is
124 primarily used to indicate when an item whose current
125 location is a special processing ('PROC') or shelving cart
126 ('CART') location is back in the stacks.
128 =cut
130 sub CartToShelf {
131 my ( $itemnumber ) = @_;
133 unless ( $itemnumber ) {
134 croak "FAILED CartToShelf() - no itemnumber supplied";
137 my $item = Koha::Items->find($itemnumber);
138 if ( $item->location eq 'CART' ) {
139 ModItem({ location => $item->permanent_location}, undef, $itemnumber);
143 =head2 AddItemFromMarc
145 my ($biblionumber, $biblioitemnumber, $itemnumber)
146 = AddItemFromMarc($source_item_marc, $biblionumber);
148 Given a MARC::Record object containing an embedded item
149 record and a biblionumber, create a new item record.
151 =cut
153 sub AddItemFromMarc {
154 my ( $source_item_marc, $biblionumber ) = @_;
155 my $dbh = C4::Context->dbh;
157 # parse item hash from MARC
158 my $frameworkcode = C4::Biblio::GetFrameworkCode($biblionumber);
159 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
161 my $localitemmarc = MARC::Record->new;
162 $localitemmarc->append_fields( $source_item_marc->field($itemtag) );
163 my $item = C4::Biblio::TransformMarcToKoha( $localitemmarc, $frameworkcode, 'items' );
164 my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
165 return AddItem( $item, $biblionumber, $dbh, $frameworkcode, $unlinked_item_subfields );
168 =head2 AddItem
170 my ($biblionumber, $biblioitemnumber, $itemnumber)
171 = AddItem($item, $biblionumber[, $dbh, $frameworkcode, $unlinked_item_subfields]);
173 Given a hash containing item column names as keys,
174 create a new Koha item record.
176 The first two optional parameters (C<$dbh> and C<$frameworkcode>)
177 do not need to be supplied for general use; they exist
178 simply to allow them to be picked up from AddItemFromMarc.
180 The final optional parameter, C<$unlinked_item_subfields>, contains
181 an arrayref containing subfields present in the original MARC
182 representation of the item (e.g., from the item editor) that are
183 not mapped to C<items> columns directly but should instead
184 be stored in C<items.more_subfields_xml> and included in
185 the biblio items tag for display and indexing.
187 =cut
189 sub AddItem {
190 my $item = shift;
191 my $biblionumber = shift;
193 my $dbh = @_ ? shift : C4::Context->dbh;
194 my $frameworkcode = @_ ? shift : C4::Biblio::GetFrameworkCode($biblionumber);
195 my $unlinked_item_subfields;
196 if (@_) {
197 $unlinked_item_subfields = shift;
200 # needs old biblionumber and biblioitemnumber
201 $item->{'biblionumber'} = $biblionumber;
202 my $sth = $dbh->prepare("SELECT biblioitemnumber FROM biblioitems WHERE biblionumber=?");
203 $sth->execute( $item->{'biblionumber'} );
204 ( $item->{'biblioitemnumber'} ) = $sth->fetchrow;
206 _set_defaults_for_add($item);
207 _set_derived_columns_for_add($item);
208 $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields);
210 # FIXME - checks here
211 unless ( $item->{itype} ) { # default to biblioitem.itemtype if no itype
212 my $itype_sth = $dbh->prepare("SELECT itemtype FROM biblioitems WHERE biblionumber = ?");
213 $itype_sth->execute( $item->{'biblionumber'} );
214 ( $item->{'itype'} ) = $itype_sth->fetchrow_array;
217 my ( $itemnumber, $error ) = _koha_new_item( $item, $item->{barcode} );
218 return if $error;
220 $item->{'itemnumber'} = $itemnumber;
222 C4::Biblio::ModZebra( $item->{biblionumber}, "specialUpdate", "biblioserver" );
224 logaction( "CATALOGUING", "ADD", $itemnumber, "item" )
225 if C4::Context->preference("CataloguingLog");
227 _after_item_action_hooks({ action => 'create', item_id => $itemnumber });
229 return ( $item->{biblionumber}, $item->{biblioitemnumber}, $itemnumber );
232 =head2 AddItemBatchFromMarc
234 ($itemnumber_ref, $error_ref) = AddItemBatchFromMarc($record,
235 $biblionumber, $biblioitemnumber, $frameworkcode);
237 Efficiently create item records from a MARC biblio record with
238 embedded item fields. This routine is suitable for batch jobs.
240 This API assumes that the bib record has already been
241 saved to the C<biblio> and C<biblioitems> tables. It does
242 not expect that C<biblio_metadata.metadata> is populated, but it
243 will do so via a call to ModBibiloMarc.
245 The goal of this API is to have a similar effect to using AddBiblio
246 and AddItems in succession, but without inefficient repeated
247 parsing of the MARC XML bib record.
249 This function returns an arrayref of new itemsnumbers and an arrayref of item
250 errors encountered during the processing. Each entry in the errors
251 list is a hashref containing the following keys:
253 =over
255 =item item_sequence
257 Sequence number of original item tag in the MARC record.
259 =item item_barcode
261 Item barcode, provide to assist in the construction of
262 useful error messages.
264 =item error_code
266 Code representing the error condition. Can be 'duplicate_barcode',
267 'invalid_homebranch', or 'invalid_holdingbranch'.
269 =item error_information
271 Additional information appropriate to the error condition.
273 =back
275 =cut
277 sub AddItemBatchFromMarc {
278 my ($record, $biblionumber, $biblioitemnumber, $frameworkcode) = @_;
279 my $error;
280 my @itemnumbers = ();
281 my @errors = ();
282 my $dbh = C4::Context->dbh;
284 # We modify the record, so lets work on a clone so we don't change the
285 # original.
286 $record = $record->clone();
287 # loop through the item tags and start creating items
288 my @bad_item_fields = ();
289 my ($itemtag, $itemsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
290 my $item_sequence_num = 0;
291 ITEMFIELD: foreach my $item_field ($record->field($itemtag)) {
292 $item_sequence_num++;
293 # we take the item field and stick it into a new
294 # MARC record -- this is required so far because (FIXME)
295 # TransformMarcToKoha requires a MARC::Record, not a MARC::Field
296 # and there is no TransformMarcFieldToKoha
297 my $temp_item_marc = MARC::Record->new();
298 $temp_item_marc->append_fields($item_field);
300 # add biblionumber and biblioitemnumber
301 my $item = TransformMarcToKoha( $temp_item_marc, $frameworkcode, 'items' );
302 my $unlinked_item_subfields = _get_unlinked_item_subfields($temp_item_marc, $frameworkcode);
303 $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields);
304 $item->{'biblionumber'} = $biblionumber;
305 $item->{'biblioitemnumber'} = $biblioitemnumber;
307 # check for duplicate barcode
308 my %item_errors = CheckItemPreSave($item);
309 if (%item_errors) {
310 push @errors, _repack_item_errors($item_sequence_num, $item, \%item_errors);
311 push @bad_item_fields, $item_field;
312 next ITEMFIELD;
315 _set_defaults_for_add($item);
316 _set_derived_columns_for_add($item);
317 my ( $itemnumber, $error ) = _koha_new_item( $item, $item->{barcode} );
318 warn $error if $error;
319 push @itemnumbers, $itemnumber; # FIXME not checking error
320 $item->{'itemnumber'} = $itemnumber;
322 logaction("CATALOGUING", "ADD", $itemnumber, "item") if C4::Context->preference("CataloguingLog");
324 my $new_item_marc = _marc_from_item_hash($item, $frameworkcode, $unlinked_item_subfields);
325 $item_field->replace_with($new_item_marc->field($itemtag));
328 # remove any MARC item fields for rejected items
329 foreach my $item_field (@bad_item_fields) {
330 $record->delete_field($item_field);
333 # update the MARC biblio
334 # $biblionumber = ModBiblioMarc( $record, $biblionumber, $frameworkcode );
336 return (\@itemnumbers, \@errors);
339 =head2 ModItemFromMarc
341 ModItemFromMarc($item_marc, $biblionumber, $itemnumber);
343 This function updates an item record based on a supplied
344 C<MARC::Record> object containing an embedded item field.
345 This API is meant for the use of C<additem.pl>; for
346 other purposes, C<ModItem> should be used.
348 This function uses the hash %default_values_for_mod_from_marc,
349 which contains default values for item fields to
350 apply when modifying an item. This is needed because
351 if an item field's value is cleared, TransformMarcToKoha
352 does not include the column in the
353 hash that's passed to ModItem, which without
354 use of this hash makes it impossible to clear
355 an item field's value. See bug 2466.
357 Note that only columns that can be directly
358 changed from the cataloging and serials
359 item editors are included in this hash.
361 Returns item record
363 =cut
365 sub _build_default_values_for_mod_marc {
366 # Has no framework parameter anymore, since Default is authoritative
367 # for Koha to MARC mappings.
369 my $cache = Koha::Caches->get_instance();
370 my $cache_key = "default_value_for_mod_marc-";
371 my $cached = $cache->get_from_cache($cache_key);
372 return $cached if $cached;
374 my $default_values = {
375 barcode => undef,
376 booksellerid => undef,
377 ccode => undef,
378 'items.cn_source' => undef,
379 coded_location_qualifier => undef,
380 copynumber => undef,
381 damaged => 0,
382 enumchron => undef,
383 holdingbranch => undef,
384 homebranch => undef,
385 itemcallnumber => undef,
386 itemlost => 0,
387 itemnotes => undef,
388 itemnotes_nonpublic => undef,
389 itype => undef,
390 location => undef,
391 permanent_location => undef,
392 materials => undef,
393 new_status => undef,
394 notforloan => 0,
395 price => undef,
396 replacementprice => undef,
397 replacementpricedate => undef,
398 restricted => undef,
399 stack => undef,
400 stocknumber => undef,
401 uri => undef,
402 withdrawn => 0,
404 my %default_values_for_mod_from_marc;
405 while ( my ( $field, $default_value ) = each %$default_values ) {
406 my $kohafield = $field;
407 $kohafield =~ s|^([^\.]+)$|items.$1|;
408 $default_values_for_mod_from_marc{$field} = $default_value
409 if C4::Biblio::GetMarcFromKohaField( $kohafield );
412 $cache->set_in_cache($cache_key, \%default_values_for_mod_from_marc);
413 return \%default_values_for_mod_from_marc;
416 sub ModItemFromMarc {
417 my $item_marc = shift;
418 my $biblionumber = shift;
419 my $itemnumber = shift;
421 my $frameworkcode = C4::Biblio::GetFrameworkCode($biblionumber);
422 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
424 my $localitemmarc = MARC::Record->new;
425 $localitemmarc->append_fields( $item_marc->field($itemtag) );
426 my $item = TransformMarcToKoha( $localitemmarc, $frameworkcode, 'items' );
427 my $default_values = _build_default_values_for_mod_marc();
428 foreach my $item_field ( keys %$default_values ) {
429 $item->{$item_field} = $default_values->{$item_field}
430 unless exists $item->{$item_field};
432 my $unlinked_item_subfields = _get_unlinked_item_subfields( $localitemmarc, $frameworkcode );
434 ModItem( $item, $biblionumber, $itemnumber, { unlinked_item_subfields => $unlinked_item_subfields } );
435 return $item;
438 =head2 ModItem
440 ModItem(
441 { column => $newvalue },
442 $biblionumber,
443 $itemnumber,
445 [ unlinked_item_subfields => $unlinked_item_subfields, ]
446 [ log_action => 1, ]
450 Change one or more columns in an item record.
452 The first argument is a hashref mapping from item column
453 names to the new values. The second and third arguments
454 are the biblionumber and itemnumber, respectively.
455 The fourth, optional parameter (additional_params) may contain the keys
456 unlinked_item_subfields and log_action.
458 C<$unlinked_item_subfields> contains an arrayref containing
459 subfields present in the original MARC
460 representation of the item (e.g., from the item editor) that are
461 not mapped to C<items> columns directly but should instead
462 be stored in C<items.more_subfields_xml> and included in
463 the biblio items tag for display and indexing.
465 If one of the changed columns is used to calculate
466 the derived value of a column such as C<items.cn_sort>,
467 this routine will perform the necessary calculation
468 and set the value.
470 If log_action is set to false, the action will not be logged.
471 If log_action is true or undefined, the action will be logged.
473 =cut
475 sub ModItem {
476 my ( $item, $biblionumber, $itemnumber, $additional_params ) = @_;
477 my $log_action = $additional_params->{log_action} // 1;
478 my $unlinked_item_subfields = $additional_params->{unlinked_item_subfields};
480 return unless %$item;
481 $item->{'itemnumber'} = $itemnumber or return;
483 # if $biblionumber is undefined, get it from the current item
484 unless (defined $biblionumber) {
485 $biblionumber = _get_single_item_column('biblionumber', $itemnumber);
488 if ($unlinked_item_subfields) {
489 $item->{'more_subfields_xml'} = _get_unlinked_subfields_xml($unlinked_item_subfields);
492 my @fields = qw( itemlost withdrawn damaged );
494 # Only retrieve the item if we need to set an "on" date field
495 if ( $item->{itemlost} || $item->{withdrawn} || $item->{damaged} ) {
496 my $pre_mod_item = Koha::Items->find( $item->{'itemnumber'} );
497 for my $field (@fields) {
498 if ( defined( $item->{$field} )
499 and not $pre_mod_item->$field
500 and $item->{$field} )
502 $item->{ $field . '_on' } =
503 DateTime::Format::MySQL->format_datetime( dt_from_string() );
508 # If the field is defined but empty, we are removing and,
509 # and thus need to clear out the 'on' field as well
510 for my $field (@fields) {
511 if ( defined( $item->{$field} ) && !$item->{$field} ) {
512 $item->{ $field . '_on' } = undef;
517 _set_derived_columns_for_mod($item);
518 _do_column_fixes_for_mod($item);
519 # FIXME add checks
520 # duplicate barcode
521 # attempt to change itemnumber
522 # attempt to change biblionumber (if we want
523 # an API to relink an item to a different bib,
524 # it should be a separate function)
526 # update items table
527 _koha_modify_item($item);
529 # request that bib be reindexed so that searching on current
530 # item status is possible
531 ModZebra( $biblionumber, "specialUpdate", "biblioserver" );
533 _after_item_action_hooks({ action => 'modify', item_id => $itemnumber });
535 logaction( "CATALOGUING", "MODIFY", $itemnumber, "item " . Dumper($item) )
536 if $log_action && C4::Context->preference("CataloguingLog");
539 =head2 ModItemTransfer
541 ModItemTransfer($itemnumber, $frombranch, $tobranch, $trigger);
543 Marks an item as being transferred from one branch to another and records the trigger.
545 =cut
547 sub ModItemTransfer {
548 my ( $itemnumber, $frombranch, $tobranch, $trigger ) = @_;
550 my $dbh = C4::Context->dbh;
551 my $item = Koha::Items->find( $itemnumber );
553 # Remove the 'shelving cart' location status if it is being used.
554 CartToShelf( $itemnumber ) if $item->location && $item->location eq 'CART' && ( !$item->permanent_location || $item->permanent_location ne 'CART' );
556 $dbh->do("UPDATE branchtransfers SET datearrived = NOW(), comments = ? WHERE itemnumber = ? AND datearrived IS NULL", undef, "Canceled, new transfer from $frombranch to $tobranch created", $itemnumber);
558 #new entry in branchtransfers....
559 my $sth = $dbh->prepare(
560 "INSERT INTO branchtransfers (itemnumber, frombranch, datesent, tobranch, reason)
561 VALUES (?, ?, NOW(), ?, ?)");
562 $sth->execute($itemnumber, $frombranch, $tobranch, $trigger);
564 ModItem({ holdingbranch => $frombranch }, undef, $itemnumber, { log_action => 0 });
565 ModDateLastSeen($itemnumber);
566 return;
569 =head2 ModDateLastSeen
571 ModDateLastSeen( $itemnumber, $leave_item_lost );
573 Mark item as seen. Is called when an item is issued, returned or manually marked during inventory/stocktaking.
574 C<$itemnumber> is the item number
575 C<$leave_item_lost> determines if a lost item will be found or remain lost
577 =cut
579 sub ModDateLastSeen {
580 my ( $itemnumber, $leave_item_lost ) = @_;
582 my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
584 my $params;
585 $params->{datelastseen} = $today;
586 $params->{itemlost} = 0 unless $leave_item_lost;
588 ModItem( $params, undef, $itemnumber, { log_action => 0 } );
591 =head2 DelItem
593 DelItem({ itemnumber => $itemnumber, [ biblionumber => $biblionumber ] } );
595 Exported function (core API) for deleting an item record in Koha.
597 =cut
599 sub DelItem {
600 my ( $params ) = @_;
602 my $itemnumber = $params->{itemnumber};
603 my $biblionumber = $params->{biblionumber};
605 unless ($biblionumber) {
606 my $item = Koha::Items->find( $itemnumber );
607 $biblionumber = $item ? $item->biblio->biblionumber : undef;
610 # If there is no biblionumber for the given itemnumber, there is nothing to delete
611 return 0 unless $biblionumber;
613 # FIXME check the item has no current issues
614 my $deleted = _koha_delete_item( $itemnumber );
616 ModZebra( $biblionumber, "specialUpdate", "biblioserver" );
618 _after_item_action_hooks({ action => 'delete', item_id => $itemnumber });
620 #search item field code
621 logaction("CATALOGUING", "DELETE", $itemnumber, "item") if C4::Context->preference("CataloguingLog");
622 return $deleted;
625 =head2 CheckItemPreSave
627 my $item_ref = TransformMarcToKoha($marc, 'items');
628 # do stuff
629 my %errors = CheckItemPreSave($item_ref);
630 if (exists $errors{'duplicate_barcode'}) {
631 print "item has duplicate barcode: ", $errors{'duplicate_barcode'}, "\n";
632 } elsif (exists $errors{'invalid_homebranch'}) {
633 print "item has invalid home branch: ", $errors{'invalid_homebranch'}, "\n";
634 } elsif (exists $errors{'invalid_holdingbranch'}) {
635 print "item has invalid holding branch: ", $errors{'invalid_holdingbranch'}, "\n";
636 } else {
637 print "item is OK";
640 Given a hashref containing item fields, determine if it can be
641 inserted or updated in the database. Specifically, checks for
642 database integrity issues, and returns a hash containing any
643 of the following keys, if applicable.
645 =over 2
647 =item duplicate_barcode
649 Barcode, if it duplicates one already found in the database.
651 =item invalid_homebranch
653 Home branch, if not defined in branches table.
655 =item invalid_holdingbranch
657 Holding branch, if not defined in branches table.
659 =back
661 This function does NOT implement any policy-related checks,
662 e.g., whether current operator is allowed to save an
663 item that has a given branch code.
665 =cut
667 sub CheckItemPreSave {
668 my $item_ref = shift;
670 my %errors = ();
672 # check for duplicate barcode
673 if (exists $item_ref->{'barcode'} and defined $item_ref->{'barcode'}) {
674 my $existing_item= Koha::Items->find({barcode => $item_ref->{'barcode'}});
675 if ($existing_item) {
676 if (!exists $item_ref->{'itemnumber'} # new item
677 or $item_ref->{'itemnumber'} != $existing_item->itemnumber) { # existing item
678 $errors{'duplicate_barcode'} = $item_ref->{'barcode'};
683 # check for valid home branch
684 if (exists $item_ref->{'homebranch'} and defined $item_ref->{'homebranch'}) {
685 my $home_library = Koha::Libraries->find( $item_ref->{homebranch} );
686 unless (defined $home_library) {
687 $errors{'invalid_homebranch'} = $item_ref->{'homebranch'};
691 # check for valid holding branch
692 if (exists $item_ref->{'holdingbranch'} and defined $item_ref->{'holdingbranch'}) {
693 my $holding_library = Koha::Libraries->find( $item_ref->{holdingbranch} );
694 unless (defined $holding_library) {
695 $errors{'invalid_holdingbranch'} = $item_ref->{'holdingbranch'};
699 return %errors;
703 =head1 EXPORTED SPECIAL ACCESSOR FUNCTIONS
705 The following functions provide various ways of
706 getting an item record, a set of item records, or
707 lists of authorized values for certain item fields.
709 =cut
711 =head2 GetItemsForInventory
713 ($itemlist, $iTotalRecords) = GetItemsForInventory( {
714 minlocation => $minlocation,
715 maxlocation => $maxlocation,
716 location => $location,
717 itemtype => $itemtype,
718 ignoreissued => $ignoreissued,
719 datelastseen => $datelastseen,
720 branchcode => $branchcode,
721 branch => $branch,
722 offset => $offset,
723 size => $size,
724 statushash => $statushash,
725 } );
727 Retrieve a list of title/authors/barcode/callnumber, for biblio inventory.
729 The sub returns a reference to a list of hashes, each containing
730 itemnumber, author, title, barcode, item callnumber, and date last
731 seen. It is ordered by callnumber then title.
733 The required minlocation & maxlocation parameters are used to specify a range of item callnumbers
734 the datelastseen can be used to specify that you want to see items not seen since a past date only.
735 offset & size can be used to retrieve only a part of the whole listing (defaut behaviour)
736 $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.
738 $iTotalRecords is the number of rows that would have been returned without the $offset, $size limit clause
740 =cut
742 sub GetItemsForInventory {
743 my ( $parameters ) = @_;
744 my $minlocation = $parameters->{'minlocation'} // '';
745 my $maxlocation = $parameters->{'maxlocation'} // '';
746 my $class_source = $parameters->{'class_source'} // C4::Context->preference('DefaultClassificationSource');
747 my $location = $parameters->{'location'} // '';
748 my $itemtype = $parameters->{'itemtype'} // '';
749 my $ignoreissued = $parameters->{'ignoreissued'} // '';
750 my $datelastseen = $parameters->{'datelastseen'} // '';
751 my $branchcode = $parameters->{'branchcode'} // '';
752 my $branch = $parameters->{'branch'} // '';
753 my $offset = $parameters->{'offset'} // '';
754 my $size = $parameters->{'size'} // '';
755 my $statushash = $parameters->{'statushash'} // '';
756 my $ignore_waiting_holds = $parameters->{'ignore_waiting_holds'} // '';
758 my $dbh = C4::Context->dbh;
759 my ( @bind_params, @where_strings );
761 my $min_cnsort = GetClassSort($class_source,undef,$minlocation);
762 my $max_cnsort = GetClassSort($class_source,undef,$maxlocation);
764 my $select_columns = q{
765 SELECT DISTINCT(items.itemnumber), barcode, itemcallnumber, title, author, biblio.biblionumber, biblio.frameworkcode, datelastseen, homebranch, location, notforloan, damaged, itemlost, withdrawn, stocknumber, items.cn_sort
767 my $select_count = q{SELECT COUNT(DISTINCT(items.itemnumber))};
768 my $query = q{
769 FROM items
770 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
771 LEFT JOIN biblioitems on items.biblionumber = biblioitems.biblionumber
773 if ($statushash){
774 for my $authvfield (keys %$statushash){
775 if ( scalar @{$statushash->{$authvfield}} > 0 ){
776 my $joinedvals = join ',', @{$statushash->{$authvfield}};
777 push @where_strings, "$authvfield in (" . $joinedvals . ")";
782 if ($minlocation) {
783 push @where_strings, 'items.cn_sort >= ?';
784 push @bind_params, $min_cnsort;
787 if ($maxlocation) {
788 push @where_strings, 'items.cn_sort <= ?';
789 push @bind_params, $max_cnsort;
792 if ($datelastseen) {
793 $datelastseen = output_pref({ str => $datelastseen, dateformat => 'iso', dateonly => 1 });
794 push @where_strings, '(datelastseen < ? OR datelastseen IS NULL)';
795 push @bind_params, $datelastseen;
798 if ( $location ) {
799 push @where_strings, 'items.location = ?';
800 push @bind_params, $location;
803 if ( $branchcode ) {
804 if($branch eq "homebranch"){
805 push @where_strings, 'items.homebranch = ?';
806 }else{
807 push @where_strings, 'items.holdingbranch = ?';
809 push @bind_params, $branchcode;
812 if ( $itemtype ) {
813 push @where_strings, 'biblioitems.itemtype = ?';
814 push @bind_params, $itemtype;
817 if ( $ignoreissued) {
818 $query .= "LEFT JOIN issues ON items.itemnumber = issues.itemnumber ";
819 push @where_strings, 'issues.date_due IS NULL';
822 if ( $ignore_waiting_holds ) {
823 $query .= "LEFT JOIN reserves ON items.itemnumber = reserves.itemnumber ";
824 push( @where_strings, q{(reserves.found != 'W' OR reserves.found IS NULL)} );
827 if ( @where_strings ) {
828 $query .= 'WHERE ';
829 $query .= join ' AND ', @where_strings;
831 my $count_query = $select_count . $query;
832 $query .= ' ORDER BY items.cn_sort, itemcallnumber, title';
833 $query .= " LIMIT $offset, $size" if ($offset and $size);
834 $query = $select_columns . $query;
835 my $sth = $dbh->prepare($query);
836 $sth->execute( @bind_params );
838 my @results = ();
839 my $tmpresults = $sth->fetchall_arrayref({});
840 $sth = $dbh->prepare( $count_query );
841 $sth->execute( @bind_params );
842 my ($iTotalRecords) = $sth->fetchrow_array();
844 my @avs = Koha::AuthorisedValues->search(
845 { 'marc_subfield_structures.kohafield' => { '>' => '' },
846 'me.authorised_value' => { '>' => '' },
848 { join => { category => 'marc_subfield_structures' },
849 distinct => ['marc_subfield_structures.kohafield, me.category, frameworkcode, me.authorised_value'],
850 '+select' => [ 'marc_subfield_structures.kohafield', 'marc_subfield_structures.frameworkcode', 'me.authorised_value', 'me.lib' ],
851 '+as' => [ 'kohafield', 'frameworkcode', 'authorised_value', 'lib' ],
855 my $avmapping = { map { $_->get_column('kohafield') . ',' . $_->get_column('frameworkcode') . ',' . $_->get_column('authorised_value') => $_->get_column('lib') } @avs };
857 foreach my $row (@$tmpresults) {
859 # Auth values
860 foreach (keys %$row) {
861 if (
862 defined(
863 $avmapping->{ "items.$_," . $row->{'frameworkcode'} . "," . ( $row->{$_} // q{} ) }
866 $row->{$_} = $avmapping->{"items.$_,".$row->{'frameworkcode'}.",".$row->{$_}};
869 push @results, $row;
872 return (\@results, $iTotalRecords);
875 =head2 GetItemsInfo
877 @results = GetItemsInfo($biblionumber);
879 Returns information about items with the given biblionumber.
881 C<GetItemsInfo> returns a list of references-to-hash. Each element
882 contains a number of keys. Most of them are attributes from the
883 C<biblio>, C<biblioitems>, C<items>, and C<itemtypes> tables in the
884 Koha database. Other keys include:
886 =over 2
888 =item C<$data-E<gt>{branchname}>
890 The name (not the code) of the branch to which the book belongs.
892 =item C<$data-E<gt>{datelastseen}>
894 This is simply C<items.datelastseen>, except that while the date is
895 stored in YYYY-MM-DD format in the database, here it is converted to
896 DD/MM/YYYY format. A NULL date is returned as C<//>.
898 =item C<$data-E<gt>{datedue}>
900 =item C<$data-E<gt>{class}>
902 This is the concatenation of C<biblioitems.classification>, the book's
903 Dewey code, and C<biblioitems.subclass>.
905 =item C<$data-E<gt>{ocount}>
907 I think this is the number of copies of the book available.
909 =item C<$data-E<gt>{order}>
911 If this is set, it is set to C<One Order>.
913 =back
915 =cut
917 sub GetItemsInfo {
918 my ( $biblionumber ) = @_;
919 my $dbh = C4::Context->dbh;
920 require C4::Languages;
921 my $language = C4::Languages::getlanguage();
922 my $query = "
923 SELECT items.*,
924 biblio.*,
925 biblioitems.volume,
926 biblioitems.number,
927 biblioitems.itemtype,
928 biblioitems.isbn,
929 biblioitems.issn,
930 biblioitems.publicationyear,
931 biblioitems.publishercode,
932 biblioitems.volumedate,
933 biblioitems.volumedesc,
934 biblioitems.lccn,
935 biblioitems.url,
936 items.notforloan as itemnotforloan,
937 issues.borrowernumber,
938 issues.date_due as datedue,
939 issues.onsite_checkout,
940 borrowers.cardnumber,
941 borrowers.surname,
942 borrowers.firstname,
943 borrowers.branchcode as bcode,
944 serial.serialseq,
945 serial.publisheddate,
946 itemtypes.description,
947 COALESCE( localization.translation, itemtypes.description ) AS translated_description,
948 itemtypes.notforloan as notforloan_per_itemtype,
949 holding.branchurl,
950 holding.branchcode,
951 holding.branchname,
952 holding.opac_info as holding_branch_opac_info,
953 home.opac_info as home_branch_opac_info,
954 IF(tmp_holdsqueue.itemnumber,1,0) AS has_pending_hold
955 FROM items
956 LEFT JOIN branches AS holding ON items.holdingbranch = holding.branchcode
957 LEFT JOIN branches AS home ON items.homebranch=home.branchcode
958 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
959 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
960 LEFT JOIN issues USING (itemnumber)
961 LEFT JOIN borrowers USING (borrowernumber)
962 LEFT JOIN serialitems USING (itemnumber)
963 LEFT JOIN serial USING (serialid)
964 LEFT JOIN itemtypes ON itemtypes.itemtype = "
965 . (C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype');
966 $query .= q|
967 LEFT JOIN tmp_holdsqueue USING (itemnumber)
968 LEFT JOIN localization ON itemtypes.itemtype = localization.code
969 AND localization.entity = 'itemtypes'
970 AND localization.lang = ?
973 $query .= " WHERE items.biblionumber = ? ORDER BY home.branchname, items.enumchron, LPAD( items.copynumber, 8, '0' ), items.dateaccessioned DESC" ;
974 my $sth = $dbh->prepare($query);
975 $sth->execute($language, $biblionumber);
976 my $i = 0;
977 my @results;
978 my $serial;
980 my $userenv = C4::Context->userenv;
981 my $want_not_same_branch = C4::Context->preference("IndependentBranches") && !C4::Context->IsSuperLibrarian();
982 while ( my $data = $sth->fetchrow_hashref ) {
983 if ( $data->{borrowernumber} && $want_not_same_branch) {
984 $data->{'NOTSAMEBRANCH'} = $data->{'bcode'} ne $userenv->{branch};
987 $serial ||= $data->{'serial'};
989 my $descriptions;
990 # get notforloan complete status if applicable
991 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.notforloan', authorised_value => $data->{itemnotforloan} });
992 $data->{notforloanvalue} = $descriptions->{lib} // '';
993 $data->{notforloanvalueopac} = $descriptions->{opac_description} // '';
995 # get restricted status and description if applicable
996 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.restricted', authorised_value => $data->{restricted} });
997 $data->{restrictedvalue} = $descriptions->{lib} // '';
998 $data->{restrictedvalueopac} = $descriptions->{opac_description} // '';
1000 # my stack procedures
1001 $descriptions = Koha::AuthorisedValues->get_description_by_koha_field({frameworkcode => $data->{frameworkcode}, kohafield => 'items.stack', authorised_value => $data->{stack} });
1002 $data->{stack} = $descriptions->{lib} // '';
1004 # Find the last 3 people who borrowed this item.
1005 my $sth2 = $dbh->prepare("SELECT * FROM old_issues,borrowers
1006 WHERE itemnumber = ?
1007 AND old_issues.borrowernumber = borrowers.borrowernumber
1008 ORDER BY returndate DESC
1009 LIMIT 3");
1010 $sth2->execute($data->{'itemnumber'});
1011 my $ii = 0;
1012 while (my $data2 = $sth2->fetchrow_hashref()) {
1013 $data->{"timestamp$ii"} = $data2->{'timestamp'} if $data2->{'timestamp'};
1014 $data->{"card$ii"} = $data2->{'cardnumber'} if $data2->{'cardnumber'};
1015 $data->{"borrower$ii"} = $data2->{'borrowernumber'} if $data2->{'borrowernumber'};
1016 $ii++;
1019 $results[$i] = $data;
1020 $i++;
1023 return $serial
1024 ? sort { ($b->{'publisheddate'} || $b->{'enumchron'}) cmp ($a->{'publisheddate'} || $a->{'enumchron'}) } @results
1025 : @results;
1028 =head2 GetItemsLocationInfo
1030 my @itemlocinfo = GetItemsLocationInfo($biblionumber);
1032 Returns the branch names, shelving location and itemcallnumber for each item attached to the biblio in question
1034 C<GetItemsInfo> returns a list of references-to-hash. Data returned:
1036 =over 2
1038 =item C<$data-E<gt>{homebranch}>
1040 Branch Name of the item's homebranch
1042 =item C<$data-E<gt>{holdingbranch}>
1044 Branch Name of the item's holdingbranch
1046 =item C<$data-E<gt>{location}>
1048 Item's shelving location code
1050 =item C<$data-E<gt>{location_intranet}>
1052 The intranet description for the Shelving Location as set in authorised_values 'LOC'
1054 =item C<$data-E<gt>{location_opac}>
1056 The OPAC description for the Shelving Location as set in authorised_values 'LOC'. Falls back to intranet description if no OPAC
1057 description is set.
1059 =item C<$data-E<gt>{itemcallnumber}>
1061 Item's itemcallnumber
1063 =item C<$data-E<gt>{cn_sort}>
1065 Item's call number normalized for sorting
1067 =back
1069 =cut
1071 sub GetItemsLocationInfo {
1072 my $biblionumber = shift;
1073 my @results;
1075 my $dbh = C4::Context->dbh;
1076 my $query = "SELECT a.branchname as homebranch, b.branchname as holdingbranch,
1077 location, itemcallnumber, cn_sort
1078 FROM items, branches as a, branches as b
1079 WHERE homebranch = a.branchcode AND holdingbranch = b.branchcode
1080 AND biblionumber = ?
1081 ORDER BY cn_sort ASC";
1082 my $sth = $dbh->prepare($query);
1083 $sth->execute($biblionumber);
1085 while ( my $data = $sth->fetchrow_hashref ) {
1086 my $av = Koha::AuthorisedValues->search({ category => 'LOC', authorised_value => $data->{location} });
1087 $av = $av->count ? $av->next : undef;
1088 $data->{location_intranet} = $av ? $av->lib : '';
1089 $data->{location_opac} = $av ? $av->opac_description : '';
1090 push @results, $data;
1092 return @results;
1095 =head2 GetHostItemsInfo
1097 $hostiteminfo = GetHostItemsInfo($hostfield);
1098 Returns the iteminfo for items linked to records via a host field
1100 =cut
1102 sub GetHostItemsInfo {
1103 my ($record) = @_;
1104 my @returnitemsInfo;
1106 if( !C4::Context->preference('EasyAnalyticalRecords') ) {
1107 return @returnitemsInfo;
1110 my @fields;
1111 if( C4::Context->preference('marcflavour') eq 'MARC21' ||
1112 C4::Context->preference('marcflavour') eq 'NORMARC') {
1113 @fields = $record->field('773');
1114 } elsif( C4::Context->preference('marcflavour') eq 'UNIMARC') {
1115 @fields = $record->field('461');
1118 foreach my $hostfield ( @fields ) {
1119 my $hostbiblionumber = $hostfield->subfield("0");
1120 my $linkeditemnumber = $hostfield->subfield("9");
1121 my @hostitemInfos = GetItemsInfo($hostbiblionumber);
1122 foreach my $hostitemInfo (@hostitemInfos) {
1123 if( $hostitemInfo->{itemnumber} eq $linkeditemnumber ) {
1124 push @returnitemsInfo, $hostitemInfo;
1125 last;
1129 return @returnitemsInfo;
1132 =head2 get_hostitemnumbers_of
1134 my @itemnumbers_of = get_hostitemnumbers_of($biblionumber);
1136 Given a biblionumber, return the list of corresponding itemnumbers that are linked to it via host fields
1138 Return a reference on a hash where key is a biblionumber and values are
1139 references on array of itemnumbers.
1141 =cut
1144 sub get_hostitemnumbers_of {
1145 my ($biblionumber) = @_;
1147 if( !C4::Context->preference('EasyAnalyticalRecords') ) {
1148 return ();
1151 my $marcrecord = C4::Biblio::GetMarcBiblio({ biblionumber => $biblionumber });
1152 return unless $marcrecord;
1154 my ( @returnhostitemnumbers, $tag, $biblio_s, $item_s );
1156 my $marcflavor = C4::Context->preference('marcflavour');
1157 if ( $marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC' ) {
1158 $tag = '773';
1159 $biblio_s = '0';
1160 $item_s = '9';
1162 elsif ( $marcflavor eq 'UNIMARC' ) {
1163 $tag = '461';
1164 $biblio_s = '0';
1165 $item_s = '9';
1168 foreach my $hostfield ( $marcrecord->field($tag) ) {
1169 my $hostbiblionumber = $hostfield->subfield($biblio_s);
1170 next unless $hostbiblionumber; # have tag, don't have $biblio_s subfield
1171 my $linkeditemnumber = $hostfield->subfield($item_s);
1172 if ( ! $linkeditemnumber ) {
1173 warn "ERROR biblionumber $biblionumber has 773^0, but doesn't have 9";
1174 next;
1176 my $is_from_biblio = Koha::Items->search({ itemnumber => $linkeditemnumber, biblionumber => $hostbiblionumber });
1177 push @returnhostitemnumbers, $linkeditemnumber
1178 if $is_from_biblio;
1181 return @returnhostitemnumbers;
1184 =head2 GetHiddenItemnumbers
1186 my @itemnumbers_to_hide = GetHiddenItemnumbers({ items => \@items, borcat => $category });
1188 Given a list of items it checks which should be hidden from the OPAC given
1189 the current configuration. Returns a list of itemnumbers corresponding to
1190 those that should be hidden. Optionally takes a borcat parameter for certain borrower types
1191 to be excluded
1193 =cut
1195 sub GetHiddenItemnumbers {
1196 my $params = shift;
1197 my $items = $params->{items};
1198 if (my $exceptions = C4::Context->preference('OpacHiddenItemsExceptions') and $params->{'borcat'}){
1199 foreach my $except (split(/\|/, $exceptions)){
1200 if ($params->{'borcat'} eq $except){
1201 return; # we don't hide anything for this borrower category
1205 my @resultitems;
1207 my $yaml = C4::Context->preference('OpacHiddenItems');
1208 return () if (! $yaml =~ /\S/ );
1209 $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
1210 my $hidingrules;
1211 eval {
1212 $hidingrules = YAML::Load($yaml);
1214 if ($@) {
1215 warn "Unable to parse OpacHiddenItems syspref : $@";
1216 return ();
1218 my $dbh = C4::Context->dbh;
1220 # For each item
1221 foreach my $item (@$items) {
1223 # We check each rule
1224 foreach my $field (keys %$hidingrules) {
1225 my $val;
1226 if (exists $item->{$field}) {
1227 $val = $item->{$field};
1229 else {
1230 my $query = "SELECT $field from items where itemnumber = ?";
1231 $val = $dbh->selectrow_array($query, undef, $item->{'itemnumber'});
1233 $val = '' unless defined $val;
1235 # If the results matches the values in the yaml file
1236 if (any { $val eq $_ } @{$hidingrules->{$field}}) {
1238 # We add the itemnumber to the list
1239 push @resultitems, $item->{'itemnumber'};
1241 # If at least one rule matched for an item, no need to test the others
1242 last;
1246 return @resultitems;
1249 =head1 LIMITED USE FUNCTIONS
1251 The following functions, while part of the public API,
1252 are not exported. This is generally because they are
1253 meant to be used by only one script for a specific
1254 purpose, and should not be used in any other context
1255 without careful thought.
1257 =cut
1259 =head2 GetMarcItem
1261 my $item_marc = GetMarcItem($biblionumber, $itemnumber);
1263 Returns MARC::Record of the item passed in parameter.
1264 This function is meant for use only in C<cataloguing/additem.pl>,
1265 where it is needed to support that script's MARC-like
1266 editor.
1268 =cut
1270 sub GetMarcItem {
1271 my ( $biblionumber, $itemnumber ) = @_;
1273 # GetMarcItem has been revised so that it does the following:
1274 # 1. Gets the item information from the items table.
1275 # 2. Converts it to a MARC field for storage in the bib record.
1277 # The previous behavior was:
1278 # 1. Get the bib record.
1279 # 2. Return the MARC tag corresponding to the item record.
1281 # The difference is that one treats the items row as authoritative,
1282 # while the other treats the MARC representation as authoritative
1283 # under certain circumstances.
1285 my $item = Koha::Items->find($itemnumber) or return;
1287 # Tack on 'items.' prefix to column names so that C4::Biblio::TransformKohaToMarc will work.
1288 # Also, don't emit a subfield if the underlying field is blank.
1290 return Item2Marc($item->unblessed, $biblionumber);
1293 sub Item2Marc {
1294 my ($itemrecord,$biblionumber)=@_;
1295 my $mungeditem = {
1296 map {
1297 defined($itemrecord->{$_}) && $itemrecord->{$_} ne '' ? ("items.$_" => $itemrecord->{$_}) : ()
1298 } keys %{ $itemrecord }
1300 my $framework = C4::Biblio::GetFrameworkCode( $biblionumber );
1301 my $itemmarc = C4::Biblio::TransformKohaToMarc( $mungeditem ); # Bug 21774: no_split parameter removed to allow cloned subfields
1302 my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField(
1303 "items.itemnumber", $framework,
1306 my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($mungeditem->{'items.more_subfields_xml'});
1307 if (defined $unlinked_item_subfields and $#$unlinked_item_subfields > -1) {
1308 foreach my $field ($itemmarc->field($itemtag)){
1309 $field->add_subfields(@$unlinked_item_subfields);
1312 return $itemmarc;
1315 =head1 PRIVATE FUNCTIONS AND VARIABLES
1317 The following functions are not meant to be called
1318 directly, but are documented in order to explain
1319 the inner workings of C<C4::Items>.
1321 =cut
1323 =head2 %derived_columns
1325 This hash keeps track of item columns that
1326 are strictly derived from other columns in
1327 the item record and are not meant to be set
1328 independently.
1330 Each key in the hash should be the name of a
1331 column (as named by TransformMarcToKoha). Each
1332 value should be hashref whose keys are the
1333 columns on which the derived column depends. The
1334 hashref should also contain a 'BUILDER' key
1335 that is a reference to a sub that calculates
1336 the derived value.
1338 =cut
1340 my %derived_columns = (
1341 'items.cn_sort' => {
1342 'itemcallnumber' => 1,
1343 'items.cn_source' => 1,
1344 'BUILDER' => \&_calc_items_cn_sort,
1348 =head2 _set_derived_columns_for_add
1350 _set_derived_column_for_add($item);
1352 Given an item hash representing a new item to be added,
1353 calculate any derived columns. Currently the only
1354 such column is C<items.cn_sort>.
1356 =cut
1358 sub _set_derived_columns_for_add {
1359 my $item = shift;
1361 foreach my $column (keys %derived_columns) {
1362 my $builder = $derived_columns{$column}->{'BUILDER'};
1363 my $source_values = {};
1364 foreach my $source_column (keys %{ $derived_columns{$column} }) {
1365 next if $source_column eq 'BUILDER';
1366 $source_values->{$source_column} = $item->{$source_column};
1368 $builder->($item, $source_values);
1372 =head2 _set_derived_columns_for_mod
1374 _set_derived_column_for_mod($item);
1376 Given an item hash representing a new item to be modified.
1377 calculate any derived columns. Currently the only
1378 such column is C<items.cn_sort>.
1380 This routine differs from C<_set_derived_columns_for_add>
1381 in that it needs to handle partial item records. In other
1382 words, the caller of C<ModItem> may have supplied only one
1383 or two columns to be changed, so this function needs to
1384 determine whether any of the columns to be changed affect
1385 any of the derived columns. Also, if a derived column
1386 depends on more than one column, but the caller is not
1387 changing all of then, this routine retrieves the unchanged
1388 values from the database in order to ensure a correct
1389 calculation.
1391 =cut
1393 sub _set_derived_columns_for_mod {
1394 my $item = shift;
1396 foreach my $column (keys %derived_columns) {
1397 my $builder = $derived_columns{$column}->{'BUILDER'};
1398 my $source_values = {};
1399 my %missing_sources = ();
1400 my $must_recalc = 0;
1401 foreach my $source_column (keys %{ $derived_columns{$column} }) {
1402 next if $source_column eq 'BUILDER';
1403 if (exists $item->{$source_column}) {
1404 $must_recalc = 1;
1405 $source_values->{$source_column} = $item->{$source_column};
1406 } else {
1407 $missing_sources{$source_column} = 1;
1410 if ($must_recalc) {
1411 foreach my $source_column (keys %missing_sources) {
1412 $source_values->{$source_column} = _get_single_item_column($source_column, $item->{'itemnumber'});
1414 $builder->($item, $source_values);
1419 =head2 _do_column_fixes_for_mod
1421 _do_column_fixes_for_mod($item);
1423 Given an item hashref containing one or more
1424 columns to modify, fix up certain values.
1425 Specifically, set to 0 any passed value
1426 of C<notforloan>, C<damaged>, C<itemlost>, or
1427 C<withdrawn> that is either undefined or
1428 contains the empty string.
1430 =cut
1432 sub _do_column_fixes_for_mod {
1433 my $item = shift;
1435 if (exists $item->{'notforloan'} and
1436 (not defined $item->{'notforloan'} or $item->{'notforloan'} eq '')) {
1437 $item->{'notforloan'} = 0;
1439 if (exists $item->{'damaged'} and
1440 (not defined $item->{'damaged'} or $item->{'damaged'} eq '')) {
1441 $item->{'damaged'} = 0;
1443 if (exists $item->{'itemlost'} and
1444 (not defined $item->{'itemlost'} or $item->{'itemlost'} eq '')) {
1445 $item->{'itemlost'} = 0;
1447 if (exists $item->{'withdrawn'} and
1448 (not defined $item->{'withdrawn'} or $item->{'withdrawn'} eq '')) {
1449 $item->{'withdrawn'} = 0;
1451 if (
1452 exists $item->{location}
1453 and ( !defined $item->{location}
1454 || ( $item->{location} ne 'CART' and $item->{location} ne 'PROC' ) )
1455 and not $item->{permanent_location}
1458 $item->{'permanent_location'} = $item->{'location'};
1460 if (exists $item->{'timestamp'}) {
1461 delete $item->{'timestamp'};
1465 =head2 _get_single_item_column
1467 _get_single_item_column($column, $itemnumber);
1469 Retrieves the value of a single column from an C<items>
1470 row specified by C<$itemnumber>.
1472 =cut
1474 sub _get_single_item_column {
1475 my $column = shift;
1476 my $itemnumber = shift;
1478 my $dbh = C4::Context->dbh;
1479 my $sth = $dbh->prepare("SELECT $column FROM items WHERE itemnumber = ?");
1480 $sth->execute($itemnumber);
1481 my ($value) = $sth->fetchrow();
1482 return $value;
1485 =head2 _calc_items_cn_sort
1487 _calc_items_cn_sort($item, $source_values);
1489 Helper routine to calculate C<items.cn_sort>.
1491 =cut
1493 sub _calc_items_cn_sort {
1494 my $item = shift;
1495 my $source_values = shift;
1497 $item->{'items.cn_sort'} = GetClassSort($source_values->{'items.cn_source'}, $source_values->{'itemcallnumber'}, "");
1500 =head2 _set_defaults_for_add
1502 _set_defaults_for_add($item_hash);
1504 Given an item hash representing an item to be added, set
1505 correct default values for columns whose default value
1506 is not handled by the DBMS. This includes the following
1507 columns:
1509 =over 2
1511 =item *
1513 C<items.dateaccessioned>
1515 =item *
1517 C<items.notforloan>
1519 =item *
1521 C<items.damaged>
1523 =item *
1525 C<items.itemlost>
1527 =item *
1529 C<items.withdrawn>
1531 =back
1533 =cut
1535 sub _set_defaults_for_add {
1536 my $item = shift;
1537 $item->{dateaccessioned} ||= output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
1538 $item->{$_} ||= 0 for (qw( notforloan damaged itemlost withdrawn));
1541 =head2 _koha_new_item
1543 my ($itemnumber,$error) = _koha_new_item( $item, $barcode );
1545 Perform the actual insert into the C<items> table.
1547 =cut
1549 sub _koha_new_item {
1550 my ( $item, $barcode ) = @_;
1551 my $dbh=C4::Context->dbh;
1552 my $error;
1553 $item->{permanent_location} //= $item->{location};
1554 _mod_item_dates( $item );
1555 my $query =
1556 "INSERT INTO items SET
1557 biblionumber = ?,
1558 biblioitemnumber = ?,
1559 barcode = ?,
1560 dateaccessioned = ?,
1561 booksellerid = ?,
1562 homebranch = ?,
1563 price = ?,
1564 replacementprice = ?,
1565 replacementpricedate = ?,
1566 datelastborrowed = ?,
1567 datelastseen = ?,
1568 stack = ?,
1569 notforloan = ?,
1570 damaged = ?,
1571 itemlost = ?,
1572 withdrawn = ?,
1573 itemcallnumber = ?,
1574 coded_location_qualifier = ?,
1575 restricted = ?,
1576 itemnotes = ?,
1577 itemnotes_nonpublic = ?,
1578 holdingbranch = ?,
1579 location = ?,
1580 permanent_location = ?,
1581 onloan = ?,
1582 issues = ?,
1583 renewals = ?,
1584 reserves = ?,
1585 cn_source = ?,
1586 cn_sort = ?,
1587 ccode = ?,
1588 itype = ?,
1589 materials = ?,
1590 uri = ?,
1591 enumchron = ?,
1592 more_subfields_xml = ?,
1593 copynumber = ?,
1594 stocknumber = ?,
1595 new_status = ?
1597 my $sth = $dbh->prepare($query);
1598 my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
1599 $sth->execute(
1600 $item->{'biblionumber'},
1601 $item->{'biblioitemnumber'},
1602 $barcode,
1603 $item->{'dateaccessioned'},
1604 $item->{'booksellerid'},
1605 $item->{'homebranch'},
1606 $item->{'price'},
1607 $item->{'replacementprice'},
1608 $item->{'replacementpricedate'} || $today,
1609 $item->{datelastborrowed},
1610 $item->{datelastseen} || $today,
1611 $item->{stack},
1612 $item->{'notforloan'},
1613 $item->{'damaged'},
1614 $item->{'itemlost'},
1615 $item->{'withdrawn'},
1616 $item->{'itemcallnumber'},
1617 $item->{'coded_location_qualifier'},
1618 $item->{'restricted'},
1619 $item->{'itemnotes'},
1620 $item->{'itemnotes_nonpublic'},
1621 $item->{'holdingbranch'},
1622 $item->{'location'},
1623 $item->{'permanent_location'},
1624 $item->{'onloan'},
1625 $item->{'issues'},
1626 $item->{'renewals'},
1627 $item->{'reserves'},
1628 $item->{'items.cn_source'},
1629 $item->{'items.cn_sort'},
1630 $item->{'ccode'},
1631 $item->{'itype'},
1632 $item->{'materials'},
1633 $item->{'uri'},
1634 $item->{'enumchron'},
1635 $item->{'more_subfields_xml'},
1636 $item->{'copynumber'},
1637 $item->{'stocknumber'},
1638 $item->{'new_status'},
1641 my $itemnumber;
1642 if ( defined $sth->errstr ) {
1643 $error.="ERROR in _koha_new_item $query".$sth->errstr;
1645 else {
1646 $itemnumber = $dbh->{'mysql_insertid'};
1649 return ( $itemnumber, $error );
1652 =head2 MoveItemFromBiblio
1654 MoveItemFromBiblio($itenumber, $frombiblio, $tobiblio);
1656 Moves an item from a biblio to another
1658 Returns undef if the move failed or the biblionumber of the destination record otherwise
1660 =cut
1662 sub MoveItemFromBiblio {
1663 my ($itemnumber, $frombiblio, $tobiblio) = @_;
1664 my $dbh = C4::Context->dbh;
1665 my ( $tobiblioitem ) = $dbh->selectrow_array(q|
1666 SELECT biblioitemnumber
1667 FROM biblioitems
1668 WHERE biblionumber = ?
1669 |, undef, $tobiblio );
1670 my $return = $dbh->do(q|
1671 UPDATE items
1672 SET biblioitemnumber = ?,
1673 biblionumber = ?
1674 WHERE itemnumber = ?
1675 AND biblionumber = ?
1676 |, undef, $tobiblioitem, $tobiblio, $itemnumber, $frombiblio );
1677 if ($return == 1) {
1678 ModZebra( $tobiblio, "specialUpdate", "biblioserver" );
1679 ModZebra( $frombiblio, "specialUpdate", "biblioserver" );
1680 # Checking if the item we want to move is in an order
1681 require C4::Acquisition;
1682 my $order = C4::Acquisition::GetOrderFromItemnumber($itemnumber);
1683 if ($order) {
1684 # Replacing the biblionumber within the order if necessary
1685 $order->{'biblionumber'} = $tobiblio;
1686 C4::Acquisition::ModOrder($order);
1689 # Update reserves, hold_fill_targets, tmp_holdsqueue and linktracker tables
1690 for my $table_name ( qw( reserves hold_fill_targets tmp_holdsqueue linktracker ) ) {
1691 $dbh->do( qq|
1692 UPDATE $table_name
1693 SET biblionumber = ?
1694 WHERE itemnumber = ?
1695 |, undef, $tobiblio, $itemnumber );
1697 return $tobiblio;
1699 return;
1702 =head2 ItemSafeToDelete
1704 ItemSafeToDelete( $biblionumber, $itemnumber);
1706 Exported function (core API) for checking whether an item record is safe to delete.
1708 returns 1 if the item is safe to delete,
1710 "book_on_loan" if the item is checked out,
1712 "not_same_branch" if the item is blocked by independent branches,
1714 "book_reserved" if the there are holds aganst the item, or
1716 "linked_analytics" if the item has linked analytic records.
1718 =cut
1720 sub ItemSafeToDelete {
1721 my ( $biblionumber, $itemnumber ) = @_;
1722 my $status;
1723 my $dbh = C4::Context->dbh;
1725 my $error;
1727 my $countanalytics = GetAnalyticsCount($itemnumber);
1729 my $item = Koha::Items->find($itemnumber) or return;
1731 if ($item->checkout) {
1732 $status = "book_on_loan";
1734 elsif ( defined C4::Context->userenv
1735 and !C4::Context->IsSuperLibrarian()
1736 and C4::Context->preference("IndependentBranches")
1737 and ( C4::Context->userenv->{branch} ne $item->homebranch ) )
1739 $status = "not_same_branch";
1741 else {
1742 # check it doesn't have a waiting reserve
1743 my $sth = $dbh->prepare(
1745 SELECT COUNT(*) FROM reserves
1746 WHERE (found = 'W' OR found = 'T')
1747 AND itemnumber = ?
1750 $sth->execute($itemnumber);
1751 my ($reserve) = $sth->fetchrow;
1752 if ($reserve) {
1753 $status = "book_reserved";
1755 elsif ( $countanalytics > 0 ) {
1756 $status = "linked_analytics";
1758 else {
1759 $status = 1;
1762 return $status;
1765 =head2 DelItemCheck
1767 DelItemCheck( $biblionumber, $itemnumber);
1769 Exported function (core API) for deleting an item record in Koha if there no current issue.
1771 DelItemCheck wraps ItemSafeToDelete around DelItem.
1773 =cut
1775 sub DelItemCheck {
1776 my ( $biblionumber, $itemnumber ) = @_;
1777 my $status = ItemSafeToDelete( $biblionumber, $itemnumber );
1779 if ( $status == 1 ) {
1780 DelItem(
1782 biblionumber => $biblionumber,
1783 itemnumber => $itemnumber
1787 return $status;
1790 =head2 _koha_modify_item
1792 my ($itemnumber,$error) =_koha_modify_item( $item );
1794 Perform the actual update of the C<items> row. Note that this
1795 routine accepts a hashref specifying the columns to update.
1797 =cut
1799 sub _koha_modify_item {
1800 my ( $item ) = @_;
1801 my $dbh=C4::Context->dbh;
1802 my $error;
1804 my $query = "UPDATE items SET ";
1805 my @bind;
1806 _mod_item_dates( $item );
1807 for my $key ( keys %$item ) {
1808 next if ( $key eq 'itemnumber' );
1809 $query.="$key=?,";
1810 push @bind, $item->{$key};
1812 $query =~ s/,$//;
1813 $query .= " WHERE itemnumber=?";
1814 push @bind, $item->{'itemnumber'};
1815 my $sth = $dbh->prepare($query);
1816 $sth->execute(@bind);
1817 if ( $sth->err ) {
1818 $error.="ERROR in _koha_modify_item $query: ".$sth->errstr;
1819 warn $error;
1821 return ($item->{'itemnumber'},$error);
1824 sub _mod_item_dates { # date formatting for date fields in item hash
1825 my ( $item ) = @_;
1826 return if !$item || ref($item) ne 'HASH';
1828 my @keys = grep
1829 { $_ =~ /^onloan$|^date|date$|datetime$/ }
1830 keys %$item;
1831 # Incl. dateaccessioned,replacementpricedate,datelastborrowed,datelastseen
1832 # NOTE: We do not (yet) have items fields ending with datetime
1833 # Fields with _on$ have been handled already
1835 foreach my $key ( @keys ) {
1836 next if !defined $item->{$key}; # skip undefs
1837 my $dt = eval { dt_from_string( $item->{$key} ) };
1838 # eval: dt_from_string will die on us if we pass illegal dates
1840 my $newstr;
1841 if( defined $dt && ref($dt) eq 'DateTime' ) {
1842 if( $key =~ /datetime/ ) {
1843 $newstr = DateTime::Format::MySQL->format_datetime($dt);
1844 } else {
1845 $newstr = DateTime::Format::MySQL->format_date($dt);
1848 $item->{$key} = $newstr; # might be undef to clear garbage
1852 =head2 _koha_delete_item
1854 _koha_delete_item( $itemnum );
1856 Internal function to delete an item record from the koha tables
1858 =cut
1860 sub _koha_delete_item {
1861 my ( $itemnum ) = @_;
1863 my $dbh = C4::Context->dbh;
1864 # save the deleted item to deleteditems table
1865 my $sth = $dbh->prepare("SELECT * FROM items WHERE itemnumber=?");
1866 $sth->execute($itemnum);
1867 my $data = $sth->fetchrow_hashref();
1869 # There is no item to delete
1870 return 0 unless $data;
1872 my $query = "INSERT INTO deleteditems SET ";
1873 my @bind = ();
1874 foreach my $key ( keys %$data ) {
1875 next if ( $key eq 'timestamp' ); # timestamp will be set by db
1876 $query .= "$key = ?,";
1877 push( @bind, $data->{$key} );
1879 $query =~ s/\,$//;
1880 $sth = $dbh->prepare($query);
1881 $sth->execute(@bind);
1883 # delete from items table
1884 $sth = $dbh->prepare("DELETE FROM items WHERE itemnumber=?");
1885 my $deleted = $sth->execute($itemnum);
1886 return ( $deleted == 1 ) ? 1 : 0;
1889 =head2 _marc_from_item_hash
1891 my $item_marc = _marc_from_item_hash($item, $frameworkcode[, $unlinked_item_subfields]);
1893 Given an item hash representing a complete item record,
1894 create a C<MARC::Record> object containing an embedded
1895 tag representing that item.
1897 The third, optional parameter C<$unlinked_item_subfields> is
1898 an arrayref of subfields (not mapped to C<items> fields per the
1899 framework) to be added to the MARC representation
1900 of the item.
1902 =cut
1904 sub _marc_from_item_hash {
1905 my $item = shift;
1906 my $frameworkcode = shift;
1907 my $unlinked_item_subfields;
1908 if (@_) {
1909 $unlinked_item_subfields = shift;
1912 # Tack on 'items.' prefix to column names so lookup from MARC frameworks will work
1913 # Also, don't emit a subfield if the underlying field is blank.
1914 my $mungeditem = { map { (defined($item->{$_}) and $item->{$_} ne '') ?
1915 (/^items\./ ? ($_ => $item->{$_}) : ("items.$_" => $item->{$_}))
1916 : () } keys %{ $item } };
1918 my $item_marc = MARC::Record->new();
1919 foreach my $item_field ( keys %{$mungeditem} ) {
1920 my ( $tag, $subfield ) = C4::Biblio::GetMarcFromKohaField( $item_field );
1921 next unless defined $tag and defined $subfield; # skip if not mapped to MARC field
1922 my @values = split(/\s?\|\s?/, $mungeditem->{$item_field}, -1);
1923 foreach my $value (@values){
1924 if ( my $field = $item_marc->field($tag) ) {
1925 $field->add_subfields( $subfield => $value );
1926 } else {
1927 my $add_subfields = [];
1928 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
1929 $add_subfields = $unlinked_item_subfields;
1931 $item_marc->add_fields( $tag, " ", " ", $subfield => $value, @$add_subfields );
1936 return $item_marc;
1939 =head2 _repack_item_errors
1941 Add an error message hash generated by C<CheckItemPreSave>
1942 to a list of errors.
1944 =cut
1946 sub _repack_item_errors {
1947 my $item_sequence_num = shift;
1948 my $item_ref = shift;
1949 my $error_ref = shift;
1951 my @repacked_errors = ();
1953 foreach my $error_code (sort keys %{ $error_ref }) {
1954 my $repacked_error = {};
1955 $repacked_error->{'item_sequence'} = $item_sequence_num;
1956 $repacked_error->{'item_barcode'} = exists($item_ref->{'barcode'}) ? $item_ref->{'barcode'} : '';
1957 $repacked_error->{'error_code'} = $error_code;
1958 $repacked_error->{'error_information'} = $error_ref->{$error_code};
1959 push @repacked_errors, $repacked_error;
1962 return @repacked_errors;
1965 =head2 _get_unlinked_item_subfields
1967 my $unlinked_item_subfields = _get_unlinked_item_subfields($original_item_marc, $frameworkcode);
1969 =cut
1971 sub _get_unlinked_item_subfields {
1972 my $original_item_marc = shift;
1973 my $frameworkcode = shift;
1975 my $marcstructure = C4::Biblio::GetMarcStructure(1, $frameworkcode, { unsafe => 1 });
1977 # assume that this record has only one field, and that that
1978 # field contains only the item information
1979 my $subfields = [];
1980 my @fields = $original_item_marc->fields();
1981 if ($#fields > -1) {
1982 my $field = $fields[0];
1983 my $tag = $field->tag();
1984 foreach my $subfield ($field->subfields()) {
1985 if (defined $subfield->[1] and
1986 $subfield->[1] ne '' and
1987 !$marcstructure->{$tag}->{$subfield->[0]}->{'kohafield'}) {
1988 push @$subfields, $subfield->[0] => $subfield->[1];
1992 return $subfields;
1995 =head2 _get_unlinked_subfields_xml
1997 my $unlinked_subfields_xml = _get_unlinked_subfields_xml($unlinked_item_subfields);
1999 =cut
2001 sub _get_unlinked_subfields_xml {
2002 my $unlinked_item_subfields = shift;
2004 my $xml;
2005 if (defined $unlinked_item_subfields and ref($unlinked_item_subfields) eq 'ARRAY' and $#$unlinked_item_subfields > -1) {
2006 my $marc = MARC::Record->new();
2007 # use of tag 999 is arbitrary, and doesn't need to match the item tag
2008 # used in the framework
2009 $marc->append_fields(MARC::Field->new('999', ' ', ' ', @$unlinked_item_subfields));
2010 $marc->encoding("UTF-8");
2011 $xml = $marc->as_xml("USMARC");
2014 return $xml;
2017 =head2 _parse_unlinked_item_subfields_from_xml
2019 my $unlinked_item_subfields = _parse_unlinked_item_subfields_from_xml($whole_item->{'more_subfields_xml'}):
2021 =cut
2023 sub _parse_unlinked_item_subfields_from_xml {
2024 my $xml = shift;
2025 require C4::Charset;
2026 return unless defined $xml and $xml ne "";
2027 my $marc = MARC::Record->new_from_xml(C4::Charset::StripNonXmlChars($xml),'UTF-8');
2028 my $unlinked_subfields = [];
2029 my @fields = $marc->fields();
2030 if ($#fields > -1) {
2031 foreach my $subfield ($fields[0]->subfields()) {
2032 push @$unlinked_subfields, $subfield->[0] => $subfield->[1];
2035 return $unlinked_subfields;
2038 =head2 GetAnalyticsCount
2040 $count= &GetAnalyticsCount($itemnumber)
2042 counts Usage of itemnumber in Analytical bibliorecords.
2044 =cut
2046 sub GetAnalyticsCount {
2047 my ($itemnumber) = @_;
2049 ### ZOOM search here
2050 my $query;
2051 $query= "hi=".$itemnumber;
2052 my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
2053 my ($err,$res,$result) = $searcher->simple_search_compat($query,0,10);
2054 return ($result);
2057 =head2 SearchItemsByField
2059 my $items = SearchItemsByField($field, $value);
2061 SearchItemsByField will search for items on a specific given field.
2062 For instance you can search all items with a specific stocknumber like this:
2064 my $items = SearchItemsByField('stocknumber', $stocknumber);
2066 =cut
2068 sub SearchItemsByField {
2069 my ($field, $value) = @_;
2071 my $filters = {
2072 field => $field,
2073 query => $value,
2076 my ($results) = SearchItems($filters);
2077 return $results;
2080 sub _SearchItems_build_where_fragment {
2081 my ($filter) = @_;
2083 my $dbh = C4::Context->dbh;
2085 my $where_fragment;
2086 if (exists($filter->{conjunction})) {
2087 my (@where_strs, @where_args);
2088 foreach my $f (@{ $filter->{filters} }) {
2089 my $fragment = _SearchItems_build_where_fragment($f);
2090 if ($fragment) {
2091 push @where_strs, $fragment->{str};
2092 push @where_args, @{ $fragment->{args} };
2095 my $where_str = '';
2096 if (@where_strs) {
2097 $where_str = '(' . join (' ' . $filter->{conjunction} . ' ', @where_strs) . ')';
2098 $where_fragment = {
2099 str => $where_str,
2100 args => \@where_args,
2103 } else {
2104 my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
2105 push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
2106 push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
2107 my @operators = qw(= != > < >= <= like);
2108 my $field = $filter->{field} // q{};
2109 if ( (0 < grep { $_ eq $field } @columns) or (substr($field, 0, 5) eq 'marc:') ) {
2110 my $op = $filter->{operator};
2111 my $query = $filter->{query};
2113 if (!$op or (0 == grep { $_ eq $op } @operators)) {
2114 $op = '='; # default operator
2117 my $column;
2118 if ($field =~ /^marc:(\d{3})(?:\$(\w))?$/) {
2119 my $marcfield = $1;
2120 my $marcsubfield = $2;
2121 my ($kohafield) = $dbh->selectrow_array(q|
2122 SELECT kohafield FROM marc_subfield_structure
2123 WHERE tagfield=? AND tagsubfield=? AND frameworkcode=''
2124 |, undef, $marcfield, $marcsubfield);
2126 if ($kohafield) {
2127 $column = $kohafield;
2128 } else {
2129 # MARC field is not linked to a DB field so we need to use
2130 # ExtractValue on marcxml from biblio_metadata or
2131 # items.more_subfields_xml, depending on the MARC field.
2132 my $xpath;
2133 my $sqlfield;
2134 my ($itemfield) = C4::Biblio::GetMarcFromKohaField('items.itemnumber');
2135 if ($marcfield eq $itemfield) {
2136 $sqlfield = 'more_subfields_xml';
2137 $xpath = '//record/datafield/subfield[@code="' . $marcsubfield . '"]';
2138 } else {
2139 $sqlfield = 'metadata'; # From biblio_metadata
2140 if ($marcfield < 10) {
2141 $xpath = "//record/controlfield[\@tag=\"$marcfield\"]";
2142 } else {
2143 $xpath = "//record/datafield[\@tag=\"$marcfield\"]/subfield[\@code=\"$marcsubfield\"]";
2146 $column = "ExtractValue($sqlfield, '$xpath')";
2148 } elsif ($field eq 'issues') {
2149 # Consider NULL as 0 for issues count
2150 $column = 'COALESCE(issues,0)';
2151 } else {
2152 $column = $field;
2155 if (ref $query eq 'ARRAY') {
2156 if ($op eq '=') {
2157 $op = 'IN';
2158 } elsif ($op eq '!=') {
2159 $op = 'NOT IN';
2161 $where_fragment = {
2162 str => "$column $op (" . join (',', ('?') x @$query) . ")",
2163 args => $query,
2165 } else {
2166 $where_fragment = {
2167 str => "$column $op ?",
2168 args => [ $query ],
2174 return $where_fragment;
2177 =head2 SearchItems
2179 my ($items, $total) = SearchItems($filter, $params);
2181 Perform a search among items
2183 $filter is a reference to a hash which can be a filter, or a combination of filters.
2185 A filter has the following keys:
2187 =over 2
2189 =item * field: the name of a SQL column in table items
2191 =item * query: the value to search in this column
2193 =item * operator: comparison operator. Can be one of = != > < >= <= like
2195 =back
2197 A combination of filters hash the following keys:
2199 =over 2
2201 =item * conjunction: 'AND' or 'OR'
2203 =item * filters: array ref of filters
2205 =back
2207 $params is a reference to a hash that can contain the following parameters:
2209 =over 2
2211 =item * rows: Number of items to return. 0 returns everything (default: 0)
2213 =item * page: Page to return (return items from (page-1)*rows to (page*rows)-1)
2214 (default: 1)
2216 =item * sortby: A SQL column name in items table to sort on
2218 =item * sortorder: 'ASC' or 'DESC'
2220 =back
2222 =cut
2224 sub SearchItems {
2225 my ($filter, $params) = @_;
2227 $filter //= {};
2228 $params //= {};
2229 return unless ref $filter eq 'HASH';
2230 return unless ref $params eq 'HASH';
2232 # Default parameters
2233 $params->{rows} ||= 0;
2234 $params->{page} ||= 1;
2235 $params->{sortby} ||= 'itemnumber';
2236 $params->{sortorder} ||= 'ASC';
2238 my ($where_str, @where_args);
2239 my $where_fragment = _SearchItems_build_where_fragment($filter);
2240 if ($where_fragment) {
2241 $where_str = $where_fragment->{str};
2242 @where_args = @{ $where_fragment->{args} };
2245 my $dbh = C4::Context->dbh;
2246 my $query = q{
2247 SELECT SQL_CALC_FOUND_ROWS items.*
2248 FROM items
2249 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2250 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber
2251 LEFT JOIN biblio_metadata ON biblio_metadata.biblionumber = biblio.biblionumber
2252 WHERE 1
2254 if (defined $where_str and $where_str ne '') {
2255 $query .= qq{ AND $where_str };
2258 $query .= q{ AND biblio_metadata.format = 'marcxml' AND biblio_metadata.schema = ? };
2259 push @where_args, C4::Context->preference('marcflavour');
2261 my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
2262 push @columns, Koha::Database->new()->schema()->resultset('Biblio')->result_source->columns;
2263 push @columns, Koha::Database->new()->schema()->resultset('Biblioitem')->result_source->columns;
2264 my $sortby = (0 < grep {$params->{sortby} eq $_} @columns)
2265 ? $params->{sortby} : 'itemnumber';
2266 my $sortorder = (uc($params->{sortorder}) eq 'ASC') ? 'ASC' : 'DESC';
2267 $query .= qq{ ORDER BY $sortby $sortorder };
2269 my $rows = $params->{rows};
2270 my @limit_args;
2271 if ($rows > 0) {
2272 my $offset = $rows * ($params->{page}-1);
2273 $query .= qq { LIMIT ?, ? };
2274 push @limit_args, $offset, $rows;
2277 my $sth = $dbh->prepare($query);
2278 my $rv = $sth->execute(@where_args, @limit_args);
2280 return unless ($rv);
2281 my ($total_rows) = $dbh->selectrow_array(q{ SELECT FOUND_ROWS() });
2283 return ($sth->fetchall_arrayref({}), $total_rows);
2287 =head1 OTHER FUNCTIONS
2289 =head2 _find_value
2291 ($indicators, $value) = _find_value($tag, $subfield, $record,$encoding);
2293 Find the given $subfield in the given $tag in the given
2294 MARC::Record $record. If the subfield is found, returns
2295 the (indicators, value) pair; otherwise, (undef, undef) is
2296 returned.
2298 PROPOSITION :
2299 Such a function is used in addbiblio AND additem and serial-edit and maybe could be used in Authorities.
2300 I suggest we export it from this module.
2302 =cut
2304 sub _find_value {
2305 my ( $tagfield, $insubfield, $record, $encoding ) = @_;
2306 my @result;
2307 my $indicator;
2308 if ( $tagfield < 10 ) {
2309 if ( $record->field($tagfield) ) {
2310 push @result, $record->field($tagfield)->data();
2311 } else {
2312 push @result, "";
2314 } else {
2315 foreach my $field ( $record->field($tagfield) ) {
2316 my @subfields = $field->subfields();
2317 foreach my $subfield (@subfields) {
2318 if ( @$subfield[0] eq $insubfield ) {
2319 push @result, @$subfield[1];
2320 $indicator = $field->indicator(1) . $field->indicator(2);
2325 return ( $indicator, @result );
2329 =head2 PrepareItemrecordDisplay
2331 PrepareItemrecordDisplay($itemrecord,$bibnum,$itemumber,$frameworkcode);
2333 Returns a hash with all the fields for Display a given item data in a template
2335 The $frameworkcode returns the item for the given frameworkcode, ONLY if bibnum is not provided
2337 =cut
2339 sub PrepareItemrecordDisplay {
2341 my ( $bibnum, $itemnum, $defaultvalues, $frameworkcode ) = @_;
2343 my $dbh = C4::Context->dbh;
2344 $frameworkcode = C4::Biblio::GetFrameworkCode($bibnum) if $bibnum;
2345 my ( $itemtagfield, $itemtagsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
2347 # Note: $tagslib obtained from GetMarcStructure() in 'unsafe' mode is
2348 # a shared data structure. No plugin (including custom ones) should change
2349 # its contents. See also GetMarcStructure.
2350 my $tagslib = GetMarcStructure( 1, $frameworkcode, { unsafe => 1 } );
2352 # return nothing if we don't have found an existing framework.
2353 return q{} unless $tagslib;
2354 my $itemrecord;
2355 if ($itemnum) {
2356 $itemrecord = C4::Items::GetMarcItem( $bibnum, $itemnum );
2358 my @loop_data;
2360 my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
2361 my $query = qq{
2362 SELECT authorised_value,lib FROM authorised_values
2364 $query .= qq{
2365 LEFT JOIN authorised_values_branches ON ( id = av_id )
2366 } if $branch_limit;
2367 $query .= qq{
2368 WHERE category = ?
2370 $query .= qq{ AND ( branchcode = ? OR branchcode IS NULL )} if $branch_limit;
2371 $query .= qq{ ORDER BY lib};
2372 my $authorised_values_sth = $dbh->prepare( $query );
2373 foreach my $tag ( sort keys %{$tagslib} ) {
2374 if ( $tag ne '' ) {
2376 # loop through each subfield
2377 my $cntsubf;
2378 foreach my $subfield ( sort keys %{ $tagslib->{$tag} } ) {
2379 next if IsMarcStructureInternal($tagslib->{$tag}{$subfield});
2380 next unless ( $tagslib->{$tag}->{$subfield}->{'tab'} );
2381 next if ( $tagslib->{$tag}->{$subfield}->{'tab'} ne "10" );
2382 my %subfield_data;
2383 $subfield_data{tag} = $tag;
2384 $subfield_data{subfield} = $subfield;
2385 $subfield_data{countsubfield} = $cntsubf++;
2386 $subfield_data{kohafield} = $tagslib->{$tag}->{$subfield}->{'kohafield'};
2387 $subfield_data{id} = "tag_".$tag."_subfield_".$subfield."_".int(rand(1000000));
2389 # $subfield_data{marc_lib}=$tagslib->{$tag}->{$subfield}->{lib};
2390 $subfield_data{marc_lib} = $tagslib->{$tag}->{$subfield}->{lib};
2391 $subfield_data{mandatory} = $tagslib->{$tag}->{$subfield}->{mandatory};
2392 $subfield_data{repeatable} = $tagslib->{$tag}->{$subfield}->{repeatable};
2393 $subfield_data{hidden} = "display:none"
2394 if ( ( $tagslib->{$tag}->{$subfield}->{hidden} > 4 )
2395 || ( $tagslib->{$tag}->{$subfield}->{hidden} < -4 ) );
2396 my ( $x, $defaultvalue );
2397 if ($itemrecord) {
2398 ( $x, $defaultvalue ) = _find_value( $tag, $subfield, $itemrecord );
2400 $defaultvalue = $tagslib->{$tag}->{$subfield}->{defaultvalue} unless $defaultvalue;
2401 if ( !defined $defaultvalue ) {
2402 $defaultvalue = q||;
2403 } else {
2404 $defaultvalue =~ s/"/&quot;/g;
2407 my $maxlength = $tagslib->{$tag}->{$subfield}->{maxlength};
2409 # search for itemcallnumber if applicable
2410 if ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
2411 && C4::Context->preference('itemcallnumber') && $itemrecord) {
2412 foreach my $itemcn_pref (split(/,/,C4::Context->preference('itemcallnumber'))){
2413 my $CNtag = substr( $itemcn_pref, 0, 3 );
2414 next unless my $field = $itemrecord->field($CNtag);
2415 my $CNsubfields = substr( $itemcn_pref, 3 );
2416 $defaultvalue = $field->as_string( $CNsubfields, ' ');
2417 last if $defaultvalue;
2420 if ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.itemcallnumber'
2421 && $defaultvalues
2422 && $defaultvalues->{'callnumber'} ) {
2423 if( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ){
2424 # if the item record exists, only use default value if the item has no callnumber
2425 $defaultvalue = $defaultvalues->{callnumber};
2426 } elsif ( !$itemrecord and $defaultvalues ) {
2427 # if the item record *doesn't* exists, always use the default value
2428 $defaultvalue = $defaultvalues->{callnumber};
2431 if ( ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.holdingbranch' || $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.homebranch' )
2432 && $defaultvalues
2433 && $defaultvalues->{'branchcode'} ) {
2434 if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
2435 $defaultvalue = $defaultvalues->{branchcode};
2438 if ( ( $tagslib->{$tag}->{$subfield}->{kohafield} eq 'items.location' )
2439 && $defaultvalues
2440 && $defaultvalues->{'location'} ) {
2442 if ( $itemrecord and $defaultvalues and not $itemrecord->subfield($tag,$subfield) ) {
2443 # if the item record exists, only use default value if the item has no locationr
2444 $defaultvalue = $defaultvalues->{location};
2445 } elsif ( !$itemrecord and $defaultvalues ) {
2446 # if the item record *doesn't* exists, always use the default value
2447 $defaultvalue = $defaultvalues->{location};
2450 if ( $tagslib->{$tag}->{$subfield}->{authorised_value} ) {
2451 my @authorised_values;
2452 my %authorised_lib;
2454 # builds list, depending on authorised value...
2455 #---- branch
2456 if ( $tagslib->{$tag}->{$subfield}->{'authorised_value'} eq "branches" ) {
2457 if ( ( C4::Context->preference("IndependentBranches") )
2458 && !C4::Context->IsSuperLibrarian() ) {
2459 my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches WHERE branchcode = ? ORDER BY branchname" );
2460 $sth->execute( C4::Context->userenv->{branch} );
2461 push @authorised_values, ""
2462 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
2463 while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
2464 push @authorised_values, $branchcode;
2465 $authorised_lib{$branchcode} = $branchname;
2467 } else {
2468 my $sth = $dbh->prepare( "SELECT branchcode,branchname FROM branches ORDER BY branchname" );
2469 $sth->execute;
2470 push @authorised_values, ""
2471 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
2472 while ( my ( $branchcode, $branchname ) = $sth->fetchrow_array ) {
2473 push @authorised_values, $branchcode;
2474 $authorised_lib{$branchcode} = $branchname;
2478 $defaultvalue = C4::Context->userenv ? C4::Context->userenv->{branch} : undef;
2479 if ( $defaultvalues and $defaultvalues->{branchcode} ) {
2480 $defaultvalue = $defaultvalues->{branchcode};
2483 #----- itemtypes
2484 } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "itemtypes" ) {
2485 my $itemtypes = Koha::ItemTypes->search_with_localization;
2486 push @authorised_values, ""
2487 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
2488 while ( my $itemtype = $itemtypes->next ) {
2489 push @authorised_values, $itemtype->itemtype;
2490 $authorised_lib{$itemtype->itemtype} = $itemtype->translated_description;
2492 if ($defaultvalues && $defaultvalues->{'itemtype'}) {
2493 $defaultvalue = $defaultvalues->{'itemtype'};
2496 #---- class_sources
2497 } elsif ( $tagslib->{$tag}->{$subfield}->{authorised_value} eq "cn_source" ) {
2498 push @authorised_values, "" unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
2500 my $class_sources = GetClassSources();
2501 my $default_source = $defaultvalue || C4::Context->preference("DefaultClassificationSource");
2503 foreach my $class_source (sort keys %$class_sources) {
2504 next unless $class_sources->{$class_source}->{'used'} or
2505 ($class_source eq $default_source);
2506 push @authorised_values, $class_source;
2507 $authorised_lib{$class_source} = $class_sources->{$class_source}->{'description'};
2510 $defaultvalue = $default_source;
2512 #---- "true" authorised value
2513 } else {
2514 $authorised_values_sth->execute(
2515 $tagslib->{$tag}->{$subfield}->{authorised_value},
2516 $branch_limit ? $branch_limit : ()
2518 push @authorised_values, ""
2519 unless ( $tagslib->{$tag}->{$subfield}->{mandatory} );
2520 while ( my ( $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
2521 push @authorised_values, $value;
2522 $authorised_lib{$value} = $lib;
2525 $subfield_data{marc_value} = {
2526 type => 'select',
2527 values => \@authorised_values,
2528 default => $defaultvalue // q{},
2529 labels => \%authorised_lib,
2531 } elsif ( $tagslib->{$tag}->{$subfield}->{value_builder} ) {
2532 # it is a plugin
2533 require Koha::FrameworkPlugin;
2534 my $plugin = Koha::FrameworkPlugin->new({
2535 name => $tagslib->{$tag}->{$subfield}->{value_builder},
2536 item_style => 1,
2538 my $pars = { dbh => $dbh, record => undef, tagslib =>$tagslib, id => $subfield_data{id}, tabloop => undef };
2539 $plugin->build( $pars );
2540 if ( $itemrecord and my $field = $itemrecord->field($tag) ) {
2541 $defaultvalue = $field->subfield($subfield) || q{};
2543 if( !$plugin->errstr ) {
2544 #TODO Move html to template; see report 12176/13397
2545 my $tab= $plugin->noclick? '-1': '';
2546 my $class= $plugin->noclick? ' disabled': '';
2547 my $title= $plugin->noclick? 'No popup': 'Tag editor';
2548 $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;
2549 } else {
2550 warn $plugin->errstr;
2551 $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
2554 elsif ( $tag eq '' ) { # it's an hidden field
2555 $subfield_data{marc_value} = qq(<input type="hidden" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
2557 elsif ( $tagslib->{$tag}->{$subfield}->{'hidden'} ) { # FIXME: shouldn't input type be "hidden" ?
2558 $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
2560 elsif ( length($defaultvalue) > 100
2561 or (C4::Context->preference("marcflavour") eq "UNIMARC" and
2562 300 <= $tag && $tag < 400 && $subfield eq 'a' )
2563 or (C4::Context->preference("marcflavour") eq "MARC21" and
2564 500 <= $tag && $tag < 600 )
2566 # oversize field (textarea)
2567 $subfield_data{marc_value} = qq(<textarea id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength">$defaultvalue</textarea>\n");
2568 } else {
2569 $subfield_data{marc_value} = qq(<input type="text" id="$subfield_data{id}" name="field_value" class="input_marceditor" size="50" maxlength="$maxlength" value="$defaultvalue" />);
2571 push( @loop_data, \%subfield_data );
2575 my $itemnumber;
2576 if ( $itemrecord && $itemrecord->field($itemtagfield) ) {
2577 $itemnumber = $itemrecord->subfield( $itemtagfield, $itemtagsubfield );
2579 return {
2580 'itemtagfield' => $itemtagfield,
2581 'itemtagsubfield' => $itemtagsubfield,
2582 'itemnumber' => $itemnumber,
2583 'iteminformation' => \@loop_data
2587 sub ToggleNewStatus {
2588 my ( $params ) = @_;
2589 my @rules = @{ $params->{rules} };
2590 my $report_only = $params->{report_only};
2592 my $dbh = C4::Context->dbh;
2593 my @errors;
2594 my @item_columns = map { "items.$_" } Koha::Items->columns;
2595 my @biblioitem_columns = map { "biblioitems.$_" } Koha::Biblioitems->columns;
2596 my $report;
2597 for my $rule ( @rules ) {
2598 my $age = $rule->{age};
2599 my $conditions = $rule->{conditions};
2600 my $substitutions = $rule->{substitutions};
2601 foreach ( @$substitutions ) {
2602 ( $_->{item_field} ) = ( $_->{field} =~ /items\.(.*)/ );
2604 my @params;
2606 my $query = q|
2607 SELECT items.*
2608 FROM items
2609 LEFT JOIN biblioitems ON biblioitems.biblionumber = items.biblionumber
2610 WHERE 1
2612 for my $condition ( @$conditions ) {
2613 if (
2614 grep { $_ eq $condition->{field} } @item_columns
2615 or grep { $_ eq $condition->{field} } @biblioitem_columns
2617 if ( $condition->{value} =~ /\|/ ) {
2618 my @values = split /\|/, $condition->{value};
2619 $query .= qq| AND $condition->{field} IN (|
2620 . join( ',', ('?') x scalar @values )
2621 . q|)|;
2622 push @params, @values;
2623 } else {
2624 $query .= qq| AND $condition->{field} = ?|;
2625 push @params, $condition->{value};
2629 if ( defined $age ) {
2630 $query .= q| AND TO_DAYS(NOW()) - TO_DAYS(dateaccessioned) >= ? |;
2631 push @params, $age;
2633 my $sth = $dbh->prepare($query);
2634 $sth->execute( @params );
2635 while ( my $values = $sth->fetchrow_hashref ) {
2636 my $biblionumber = $values->{biblionumber};
2637 my $itemnumber = $values->{itemnumber};
2638 for my $substitution ( @$substitutions ) {
2639 next unless $substitution->{field};
2640 next if ( defined $values->{ $substitution->{item_field} } and $values->{ $substitution->{item_field} } eq $substitution->{value} );
2641 C4::Items::ModItem( { $substitution->{item_field} => $substitution->{value} }, $biblionumber, $itemnumber )
2642 unless $report_only;
2643 push @{ $report->{$itemnumber} }, $substitution;
2648 return $report;
2651 =head2 _after_item_action_hooks
2653 Helper method that takes care of calling all plugin hooks
2655 =cut
2657 sub _after_item_action_hooks {
2658 my ( $args ) = @_;
2660 my $item_id = $args->{item_id};
2661 my $action = $args->{action};
2663 if ( C4::Context->preference('UseKohaPlugins') && C4::Context->config("enable_plugins") ) {
2665 my @plugins = Koha::Plugins->new->GetPlugins({
2666 method => 'after_item_action',
2669 if (@plugins) {
2671 my $item = Koha::Items->find( $item_id );
2673 foreach my $plugin ( @plugins ) {
2674 try {
2675 $plugin->after_item_action({ action => $action, item => $item, item_id => $item_id });
2677 catch {
2678 warn "$_";