4 # Copyright 2000-2002 Katipo Communications
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>.
23 use Try::Tiny qw( catch try );
25 use C4::Auth qw( get_template_and_user haspermission );
26 use C4::Output qw( output_html_with_http_headers );
29 GetAuthorisedValueDesc
32 IsMarcStructureInternal
35 use C4::Items qw( GetItemsInfo Item2Marc ModItemFromMarc );
36 use C4::Circulation qw( barcodedecode LostItem IsItemIssued );
39 use C4::BackgroundJob;
40 use C4::ClassSource qw( GetClassSources GetClassSource );
42 use List::MoreUtils qw( uniq );
45 use Koha::Exceptions::Exception;
46 use Koha::AuthorisedValues;
48 use Koha::DateUtils qw( dt_from_string );
52 use Koha::SearchEngine::Indexer;
53 use Koha::UI::Form::Builder::Item;
56 my $dbh = C4::Context->dbh;
57 my $error = $input->param('error');
58 my @itemnumbers = $input->multi_param('itemnumber');
59 my $biblionumber = $input->param('biblionumber');
60 my $op = $input->param('op');
61 my $del = $input->param('del');
62 my $del_records = $input->param('del_records');
63 my $src = $input->param('src');
64 my $use_default_values = $input->param('use_default_values');
65 my $exclude_from_local_holds_priority = $input->param('exclude_from_local_holds_priority');
70 $template_name = "tools/batchMod.tt";
71 $template_flag = { tools => '*' };
74 $template_name = ($del) ? "tools/batchMod-del.tt" : "tools/batchMod-edit.tt";
75 $template_flag = ($del) ? { tools => 'items_batchdel' } : { tools => 'items_batchmod' };
78 my ($template, $loggedinuser, $cookie)
79 = get_template_and_user({template_name => $template_name,
82 flagsrequired => $template_flag,
85 $template->param( searchid => scalar $input->param('searchid'), );
87 # Does the user have a restricted item edition permission?
88 my $uid = $loggedinuser ? Koha::Patrons->find( $loggedinuser )->userid : undef;
89 my $restrictededition = $uid ? haspermission($uid, {'tools' => 'items_batchmod_restricted'}) : undef;
90 # In case user is a superlibrarian, edition is not restricted
91 $restrictededition = 0 if ($restrictededition != 0 && C4::Context->IsSuperLibrarian());
93 $template->param(del => $del);
96 my @errors; # store errors found while checking data BEFORE saving item.
97 my $items_display_hashref;
98 our $tagslib = &GetMarcStructure(1);
100 my $deleted_items = 0; # Number of deleted items
101 my $deleted_records = 0; # Number of deleted records ( with no items attached )
102 my $not_deleted_items = 0; # Number of items that could not be deleted
103 my @not_deleted; # List of the itemnumbers that could not be deleted
104 my $modified_items = 0; # Numbers of modified items
105 my $modified_fields = 0; # Numbers of modified fields
107 my %cookies = parse CGI::Cookie($cookie);
108 my $sessionID = $cookies{'CGISESSID'}->value;
111 #--- ----------------------------------------------------------------------------
112 if ($op eq "action") {
113 #-------------------------------------------------------------------------------
114 my @tags = $input->multi_param('tag');
115 my @subfields = $input->multi_param('subfield');
116 my @values = $input->multi_param('field_value');
117 my @searches = $input->multi_param('regex_search');
118 my @replaces = $input->multi_param('regex_replace');
119 my @modifiers = $input->multi_param('regex_modifiers');
121 my $upd_biblionumbers;
122 my $del_biblionumbers;
125 my $schema = Koha::Database->new->schema;
128 foreach my $itemnumber (@itemnumbers) {
129 my $item = Koha::Items->find($itemnumber);
132 ; # Should have been tested earlier, but just in case...
133 my $itemdata = $item->unblessed;
134 my $return = $item->safe_delete;
135 if ( ref( $return ) ) {
137 push @$upd_biblionumbers, $itemdata->{'biblionumber'};
140 $not_deleted_items++;
143 biblionumber => $itemdata->{'biblionumber'},
144 itemnumber => $itemdata->{'itemnumber'},
145 barcode => $itemdata->{'barcode'},
146 title => $itemdata->{'title'},
151 # If there are no items left, delete the biblio
153 my $itemscount = Koha::Biblios->find( $itemdata->{'biblionumber'} )->items->count;
154 if ( $itemscount == 0 ) {
155 my $error = DelBiblio( $itemdata->{'biblionumber'}, { skip_record_index => 1 } );
158 push @$del_biblionumbers, $itemdata->{'biblionumber'};
159 if ( $src eq 'CATALOGUING' ) {
160 # We are coming catalogue/detail.pl, there were items from a single bib record
161 $template->param( biblio_deleted => 1 );
168 Koha::Exceptions::Exception->throw(
169 'Some items have not been deleted, rolling back');
176 if ( $_->isa('Koha::Exceptions::Exception') ) {
177 $template->param( deletion_failed => 1 );
179 die "Something terrible has happened!"
180 if ($_ =~ /Rollback failed/); # Rollback failed
184 else { # modification
186 my @columns = Koha::Items->columns;
189 my @columns_with_regex;
190 for my $c ( @columns ) {
191 if ( $c eq 'more_subfields_xml' ) {
192 my @more_subfields_xml = $input->multi_param("items.more_subfields_xml");
193 my @unlinked_item_subfields;
194 for my $subfield ( @more_subfields_xml ) {
195 my $v = $input->param('items.more_subfields_xml_' . $subfield);
196 push @unlinked_item_subfields, $subfield, $v;
198 if ( @unlinked_item_subfields ) {
199 my $marc = MARC::Record->new();
200 # use of tag 999 is arbitrary, and doesn't need to match the item tag
201 # used in the framework
202 $marc->append_fields(MARC::Field->new('999', ' ', ' ', @unlinked_item_subfields));
203 $marc->encoding("UTF-8");
204 # FIXME This is WRONG! We need to use the values that haven't been modified by the batch tool!
205 $new_item_data->{more_subfields_xml} = $marc->as_xml("USMARC");
208 $new_item_data->{more_subfields_xml} = undef;
209 # FIXME deal with more_subfields_xml and @subfields_to_blank
210 } elsif ( grep { $c eq $_ } @subfields_to_blank ) {
212 $new_item_data->{$c} = undef
215 my @v = grep { $_ ne "" }
216 uniq $input->multi_param( "items." . $c );
220 $new_item_data->{$c} = join ' | ', @v;
223 if ( my $regex_search = $input->param('items.'.$c.'_regex_search') ) {
224 push @columns_with_regex, $c;
229 my $schema = Koha::Database->new->schema;
233 foreach my $itemnumber (@itemnumbers) {
234 my $item = Koha::Items->find($itemnumber);
237 ; # Should have been tested earlier, but just in case...
238 my $itemdata = $item->unblessed;
240 my $modified_holds_priority = 0;
241 if ( defined $exclude_from_local_holds_priority && $exclude_from_local_holds_priority ne "" ) {
242 if(!defined $item->exclude_from_local_holds_priority || $item->exclude_from_local_holds_priority != $exclude_from_local_holds_priority) {
243 $item->exclude_from_local_holds_priority($exclude_from_local_holds_priority)->store;
244 $modified_holds_priority = 1;
249 for my $c ( @columns_with_regex ) {
250 my $regex_search = $input->param('items.'.$c.'_regex_search');
251 my $old_value = $item->$c;
253 my $value = apply_regex(
255 search => $regex_search,
256 replace => $input->param(
257 'items' . $c . '_regex_replace'
259 modifiers => $input->param(
260 'items' . $c . '_regex_modifiers'
265 unless ( $old_value eq $value ) {
271 $modified += scalar(keys %$new_item_data); # FIXME This is incorrect if old value == new value. Should we loop of the keys and compare the before/after values?
273 my $itemlost_pre = $item->itemlost;
274 $item->set($new_item_data)->store({skip_record_index => 1});
276 push @$upd_biblionumbers, $itemdata->{'biblionumber'};
279 $item->itemnumber, 'batchmod', undef,
280 { skip_record_index => 1 }
282 and not $itemlost_pre;
285 $modified_items++ if $modified || $modified_holds_priority;
286 $modified_fields += $modified + $modified_holds_priority;
293 die "Something terrible has happened!"
294 if ($_ =~ /Rollback failed/); # Rollback failed
298 $upd_biblionumbers = [ uniq @$upd_biblionumbers ]; # Only update each bib once
300 # Don't send specialUpdate for records we are going to delete
301 my %del_bib_hash = map{ $_ => undef } @$del_biblionumbers;
302 @$upd_biblionumbers = grep( ! exists( $del_bib_hash{$_} ), @$upd_biblionumbers );
304 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
305 $indexer->index_records( $upd_biblionumbers, 'specialUpdate', "biblioserver", undef ) if @$upd_biblionumbers;
306 $indexer->index_records( $del_biblionumbers, 'recordDelete', "biblioserver", undef ) if @$del_biblionumbers;
308 # Once the job is done
309 # If we have a reasonable amount of items, we display them
310 my $max_items = $del ? C4::Context->preference("MaxItemsToDisplayForBatchDel") : C4::Context->preference("MaxItemsToDisplayForBatchMod");
311 if (scalar(@itemnumbers) <= $max_items ){
312 if (scalar(@itemnumbers) <= 1000 ) {
313 $items_display_hashref=BuildItemsData(@itemnumbers);
315 # Else, we only display the barcode
316 my @simple_items_display = map {
318 my $item = Koha::Items->find($itemnumber);
320 itemnumber => $itemnumber,
321 barcode => $item ? ( $item->barcode // q{} ) : q{},
322 biblionumber => $item ? $item->biblio->biblionumber : q{},
325 $template->param("simple_items_display" => \@simple_items_display);
328 $template->param( "too_many_items_display" => scalar(@itemnumbers) );
329 $template->param( "job_completed" => 1 );
333 # Calling the template
335 modified_items => $modified_items,
336 modified_fields => $modified_fields,
341 #-------------------------------------------------------------------------------
342 # build screen with existing items. and "new" one
343 #-------------------------------------------------------------------------------
346 my $filefh = $input->upload('uploadfile');
347 my $filecontent = $input->param('filecontent');
348 my ( @notfoundbarcodes, @notfounditemnumbers);
350 my $split_chars = C4::Context->preference('BarcodeSeparators');
352 binmode $filefh, ':encoding(UTF-8)';
354 while (my $content=<$filefh>){
355 $content =~ s/[\r\n]*$//;
356 push @contentlist, $content if $content;
359 if ($filecontent eq 'barcode_file') {
360 @contentlist = grep /\S/, ( map { split /[$split_chars]/ } @contentlist );
361 @contentlist = uniq @contentlist;
362 # Note: adding lc for case insensitivity
363 my %itemdata = map { lc($_->{barcode}) => $_->{itemnumber} } @{ Koha::Items->search({ barcode => \@contentlist }, { columns => [ 'itemnumber', 'barcode' ] } )->unblessed };
364 @itemnumbers = map { exists $itemdata{lc $_} ? $itemdata{lc $_} : () } @contentlist;
365 @notfoundbarcodes = grep { !exists $itemdata{lc $_} } @contentlist;
367 elsif ( $filecontent eq 'itemid_file') {
368 @contentlist = uniq @contentlist;
369 my %itemdata = map { $_->{itemnumber} => 1 } @{ Koha::Items->search({ itemnumber => \@contentlist }, { columns => [ 'itemnumber' ] } )->unblessed };
370 @itemnumbers = grep { exists $itemdata{$_} } @contentlist;
371 @notfounditemnumbers = grep { !exists $itemdata{$_} } @contentlist;
374 if (defined $biblionumber && !@itemnumbers){
375 my @all_items = GetItemsInfo( $biblionumber );
376 foreach my $itm (@all_items) {
377 push @itemnumbers, $itm->{itemnumber};
380 if ( my $list = $input->param('barcodelist') ) {
381 my @barcodelist = grep /\S/, ( split /[$split_chars]/, $list );
382 @barcodelist = uniq @barcodelist;
384 @barcodelist = map { barcodedecode( $_ ) } @barcodelist;
386 # Note: adding lc for case insensitivity
387 my %itemdata = map { lc($_->{barcode}) => $_->{itemnumber} } @{ Koha::Items->search({ barcode => \@barcodelist }, { columns => [ 'itemnumber', 'barcode' ] } )->unblessed };
388 @itemnumbers = map { exists $itemdata{lc $_} ? $itemdata{lc $_} : () } @barcodelist;
389 @notfoundbarcodes = grep { !exists $itemdata{lc $_} } @barcodelist;
393 # Flag to tell the template there are valid results, hidden or not
394 if(scalar(@itemnumbers) > 0){ $template->param("itemresults" => 1); }
395 # Only display the items if there are no more than pref MaxItemsToProcessForBatchMod or MaxItemsToDisplayForBatchDel
396 my $max_display_items = $del
397 ? C4::Context->preference("MaxItemsToDisplayForBatchDel")
398 : C4::Context->preference("MaxItemsToDisplayForBatchMod");
399 $template->param("too_many_items_process" => scalar(@itemnumbers)) if !$del && scalar(@itemnumbers) > C4::Context->preference("MaxItemsToProcessForBatchMod");
400 if (scalar(@itemnumbers) <= ( $max_display_items // 1000 ) ) {
401 $items_display_hashref=BuildItemsData(@itemnumbers);
403 $template->param("too_many_items_display" => scalar(@itemnumbers));
404 # Even if we do not display the items, we need the itemnumbers
405 $template->param(itemnumbers_array => \@itemnumbers);
408 # now, build the item form for entering a new item
410 my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
412 my $pref_itemcallnumber = C4::Context->preference('itemcallnumber');
414 # Getting list of subfields to keep when restricted batchmod edit is enabled
415 my @subfields_to_allow = $restrictededition ? split ' ', C4::Context->preference('SubfieldsToAllowForRestrictedBatchmod') : ();
417 my $subfields = Koha::UI::Form::Builder::Item->new->edit_form(
419 restricted_editition => $restrictededition,
422 ? ( subfields_to_allow => \@subfields_to_allow )
425 subfields_to_ignore => ['items.barcode'],
426 prefill_with_default_values => $use_default_values,
427 default_branches_empty => 1,
431 # what's the next op ? it's what we are not in : an add if we're editing, otherwise, and edit.
433 subfields => $subfields,
434 notfoundbarcodes => \@notfoundbarcodes,
435 notfounditemnumbers => \@notfounditemnumbers
438 } # -- End action="show"
440 $template->param(%$items_display_hashref) if $items_display_hashref;
444 $template->param( $op => 1 ) if $op;
446 if ($op eq "action") {
448 #my @not_deleted_loop = map{{itemnumber=>$_}}@not_deleted;
451 not_deleted_items => $not_deleted_items,
452 deleted_items => $deleted_items,
453 delete_records => $del_records,
454 deleted_records => $deleted_records,
455 not_deleted_loop => \@not_deleted
459 foreach my $error (@errors) {
460 $template->param($error => 1) if $error;
462 $template->param(src => $src);
463 $template->param(biblionumber => $biblionumber);
464 output_html_with_http_headers $input, $cookie, $template->output;
468 # ---------------- Functions
472 # now, build existiing item list
473 my %witness; #---- stores the list of subfields used at least once, with the "meaning" of the code
475 #---- finds where items.itemnumber is stored
476 my ( $itemtagfield, $itemtagsubfield) = &GetMarcFromKohaField( "items.itemnumber" );
477 my ($branchtagfield, $branchtagsubfield) = &GetMarcFromKohaField( "items.homebranch" );
478 foreach my $itemnumber (@itemnumbers){
479 my $itemdata = Koha::Items->find($itemnumber);
480 next unless $itemdata; # Should have been tested earlier, but just in case...
481 $itemdata = $itemdata->unblessed;
482 my $itemmarc=Item2Marc($itemdata);
484 foreach my $field (grep {$_->tag() eq $itemtagfield} $itemmarc->fields()) {
485 # loop through each subfield
486 my $itembranchcode=$field->subfield($branchtagsubfield);
487 if ($itembranchcode && C4::Context->preference("IndependentBranches")) {
489 my $userenv = C4::Context->userenv();
490 unless (C4::Context->IsSuperLibrarian() or (($userenv->{'branch'} eq $itembranchcode))){
491 $this_row{'nomod'}=1;
494 my $tag=$field->tag();
495 foreach my $subfield ($field->subfields) {
496 my ($subfcode,$subfvalue)=@$subfield;
497 next if ($tagslib->{$tag}->{$subfcode}->{tab} ne 10
498 && $tag ne $itemtagfield
499 && $subfcode ne $itemtagsubfield);
501 $witness{$subfcode} = $tagslib->{$tag}->{$subfcode}->{lib} if ($tagslib->{$tag}->{$subfcode}->{tab} eq 10);
502 if ($tagslib->{$tag}->{$subfcode}->{tab} eq 10) {
503 $this_row{$subfcode}=GetAuthorisedValueDesc( $tag,
504 $subfcode, $subfvalue, '', $tagslib)
508 $this_row{itemnumber} = $subfvalue if ($tag eq $itemtagfield && $subfcode eq $itemtagsubfield);
512 # grab title, author, and ISBN to identify bib that the item
513 # belongs to in the display
514 my $biblio = Koha::Biblios->find( $itemdata->{biblionumber} );
515 $this_row{title} = $biblio->title;
516 $this_row{author} = $biblio->author;
517 $this_row{isbn} = $biblio->biblioitem->isbn;
518 $this_row{biblionumber} = $biblio->biblionumber;
519 $this_row{holds} = $biblio->holds->count;
520 $this_row{item_holds} = Koha::Holds->search( { itemnumber => $itemnumber } )->count;
521 $this_row{item} = Koha::Items->find($itemnumber);
524 push(@big_array, \%this_row);
527 @big_array = sort {$a->{0} cmp $b->{0}} @big_array;
529 # now, construct template !
530 # First, the existing items for display
532 my @witnesscodessorted=sort keys %witness;
533 for my $row ( @big_array ) {
535 my @item_fields = map +{ field => $_ || '' }, @$row{ @witnesscodessorted };
536 $row_data{item_value} = [ @item_fields ];
537 $row_data{itemnumber} = $row->{itemnumber};
538 #reporting this_row values
539 $row_data{'nomod'} = $row->{'nomod'};
540 $row_data{bibinfo} = $row->{bibinfo};
541 $row_data{author} = $row->{author};
542 $row_data{title} = $row->{title};
543 $row_data{isbn} = $row->{isbn};
544 $row_data{biblionumber} = $row->{biblionumber};
545 $row_data{holds} = $row->{holds};
546 $row_data{item_holds} = $row->{item_holds};
547 $row_data{item} = $row->{item};
548 $row_data{safe_to_delete} = $row->{item}->safe_to_delete;
549 my $is_on_loan = C4::Circulation::IsItemIssued( $row->{itemnumber} );
550 $row_data{onloan} = $is_on_loan ? 1 : 0;
551 push(@item_value_loop,\%row_data);
553 my @header_loop=map { { header_value=> $witness{$_}} } @witnesscodessorted;
555 my @cannot_be_deleted = map {
556 $_->{safe_to_delete} == 1 ? () : $_->{item}->barcode
559 item_loop => \@item_value_loop,
560 cannot_be_deleted => \@cannot_be_deleted,
561 item_header_loop => \@header_loop
565 #BE WARN : it is not the general case
566 # This function can be OK in the item marc record special case
567 # Where subfield is not repeated
568 # And where we are sure that field should correspond
571 my ($marcfrom,$marcto)=@_;
572 my ( $itemtag, $itemtagsubfield) = &GetMarcFromKohaField( "items.itemnumber" );
573 my $fieldfrom=$marcfrom->field($itemtag);
574 my @fields_to=$marcto->field($itemtag);
577 return $modified unless $fieldfrom;
579 foreach my $subfield ( $fieldfrom->subfields() ) {
580 foreach my $field_to_update ( @fields_to ) {
581 if ( $subfield->[1] ) {
582 unless ( $field_to_update->subfield($subfield->[0]) eq $subfield->[1] ) {
584 $field_to_update->update( $subfield->[0] => $subfield->[1] );
589 $field_to_update->delete_subfield( code => $subfield->[0] );
598 my $search = $params->{search};
599 my $replace = $params->{replace};
600 my $modifiers = $params->{modifiers} || [];
601 my $value = $params->{value};
603 my @available_modifiers = qw( i g );
604 my $retained_modifiers = q||;
605 for my $modifier ( split //, @$modifiers ) {
606 $retained_modifiers .= $modifier
607 if grep { /$modifier/ } @available_modifiers;
609 if ( $retained_modifiers =~ m/^(ig|gi)$/ ) {
610 $value =~ s/$search/$replace/ig;
612 elsif ( $retained_modifiers eq 'i' ) {
613 $value =~ s/$search/$replace/i;
615 elsif ( $retained_modifiers eq 'g' ) {
616 $value =~ s/$search/$replace/g;
619 $value =~ s/$search/$replace/;