3 # Copyright 2000-2009 Biblibre S.A
4 # John Soros <john.soros@biblibre.com>
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 #need to open cgi and get the fh before anything else opens a new cgi context (see C4::Auth)
26 my $uploadbarcodes = $input->param('uploadbarcodes');
35 use C4
::Reports
::Guided
; #_get_column_defs
40 use Koha
::AuthorisedValues
;
41 use Koha
::BiblioFrameworks
;
42 use Koha
::ClassSources
;
44 use List
::MoreUtils
qw( none );
46 my $minlocation=$input->param('minlocation') || '';
47 my $maxlocation=$input->param('maxlocation');
48 my $class_source=$input->param('class_source');
49 $maxlocation=$minlocation.'Z' unless ( $maxlocation || ! $minlocation );
50 my $location=$input->param('location') || '';
51 my $ignoreissued=$input->param('ignoreissued');
52 my $ignore_waiting_holds = $input->param('ignore_waiting_holds');
53 my $datelastseen = $input->param('datelastseen'); # last inventory date
54 my $branchcode = $input->param('branchcode') || '';
55 my $branch = $input->param('branch');
56 my $op = $input->param('op');
57 my $compareinv2barcd = $input->param('compareinv2barcd');
58 my $dont_checkin = $input->param('dont_checkin');
59 my $out_of_order = $input->param('out_of_order');
61 my ( $template, $borrowernumber, $cookie ) = get_template_and_user
(
62 { template_name
=> "tools/inventory.tt",
66 flagsrequired
=> { tools
=> 'inventory' },
71 my @authorised_value_list;
72 my $authorisedvalue_categories = '';
74 my $frameworks = Koha
::BiblioFrameworks
->search({}, { order_by
=> ['frameworktext'] })->unblessed;
75 unshift @
$frameworks, { frameworkcode
=> '' };
77 for my $fwk ( @
$frameworks ){
78 my $fwkcode = $fwk->{frameworkcode
};
79 my $mss = Koha
::MarcSubfieldStructures
->search({ frameworkcode
=> $fwkcode, kohafield
=> 'items.location', authorised_value
=> [ -and => {'!=' => undef }, {'!=' => ''}] });
80 my $authcode = $mss->count ?
$mss->next->authorised_value : undef;
81 if ($authcode && $authorisedvalue_categories!~/\b$authcode\W/){
82 $authorisedvalue_categories.="$authcode ";
83 my $data=GetAuthorisedValues
($authcode);
84 foreach my $value (@
$data){
85 $value->{selected
}=1 if ($value->{authorised_value
} eq ($location));
87 push @authorised_value_list,@
$data;
93 for my $statfield (qw
/items.notforloan items.itemlost items.withdrawn items.damaged/){
95 $hash->{fieldname
} = $statfield;
96 my $mss = Koha
::MarcSubfieldStructures
->search({ frameworkcode
=> '', kohafield
=> $statfield, authorised_value
=> [ -and => {'!=' => undef }, {'!=' => ''}] });
97 $hash->{authcode
} = $mss->count ?
$mss->next->authorised_value : undef;
98 if ($hash->{authcode
}){
99 my $arr = GetAuthorisedValues
($hash->{authcode
});
100 if ( $statfield eq 'items.notforloan') {
101 # Add notforloan == 0 to the list of possible notforloan statuses
102 # The lib value is replaced in the template
103 push @
$arr, { authorised_value
=> 0, id
=> 'stat0' , lib
=> '__IGNORE__' } if ! grep { $_->{authorised_value
} eq '0' } @
$arr;
104 @notforloans = map { $_->{'authorised_value'} } @
$arr;
106 $hash->{values} = $arr;
107 push @
$statuses, $hash;
111 $template->param( statuses
=> $statuses );
112 my $staton = {}; #authorized values that are ticked
113 for my $authvfield (@
$statuses) {
114 $staton->{$authvfield->{fieldname
}} = [];
115 for my $authval (@
{$authvfield->{values}}){
116 if ( defined $input->param('status-' . $authvfield->{fieldname
} . '-' . $authval->{authorised_value
}) && $input->param('status-' . $authvfield->{fieldname
} . '-' . $authval->{authorised_value
}) eq 'on' ){
117 push @
{$staton->{$authvfield->{fieldname
}}}, $authval->{authorised_value
};
122 my @class_sources = Koha
::ClassSources
->search({ used
=> 1 });
123 my $pref_class = C4
::Context
->preference("DefaultClassificationSource");
127 authorised_values
=> \
@authorised_value_list,
128 today
=> dt_from_string
,
129 minlocation
=> $minlocation,
130 maxlocation
=> $maxlocation,
131 location
=> $location,
132 ignoreissued
=> $ignoreissued,
133 branchcode
=> $branchcode,
135 datelastseen
=> $datelastseen,
136 compareinv2barcd
=> $compareinv2barcd,
137 uploadedbarcodesflag
=> $uploadbarcodes ?
1 : 0,
138 ignore_waiting_holds
=> $ignore_waiting_holds,
139 class_sources
=> \
@class_sources,
140 pref_class
=> $pref_class
143 # Walk through uploaded barcodes, report errors, mark as seen, check in
147 my $moddatecount = 0;
148 if ( $uploadbarcodes && length($uploadbarcodes) > 0 ) {
149 my $dbh = C4
::Context
->dbh;
150 my $date = dt_from_string
( scalar $input->param('setdate') );
151 $date = output_pref
( { dt
=> $date, dateformat
=> 'iso' } );
153 my $strsth = "select * from issues, items where items.itemnumber=issues.itemnumber and items.barcode =?";
154 my $qonloan = $dbh->prepare($strsth);
155 $strsth="select * from items where items.barcode =? and items.withdrawn = 1";
156 my $qwithdrawn = $dbh->prepare($strsth);
159 my @uploadedbarcodes;
161 my $sth = $dbh->column_info(undef,undef,"items","barcode");
162 my $barcode_def = $sth->fetchall_hashref('COLUMN_NAME');
163 my $barcode_size = $barcode_def->{barcode
}->{COLUMN_SIZE
};
167 binmode($uploadbarcodes, ":encoding(UTF-8)");
168 while (my $barcode=<$uploadbarcodes>) {
169 my $split_chars = C4
::Context
->preference('BarcodeSeparators');
170 push @uploadedbarcodes, grep { /\S/ } split( /[$split_chars]/, $barcode );
172 for my $barcode (@uploadedbarcodes) {
173 next unless $barcode;
175 if (length($barcode)>$barcode_size) {
178 my $check_barcode = $barcode;
179 $check_barcode =~ s/\p{Print}//g;
180 if (length($check_barcode)>0) { # Only printable unicode characters allowed.
183 next if length($barcode)>$barcode_size;
184 next if ( length($check_barcode)>0 );
185 push @barcodes,$barcode;
187 $template->param( LinesRead
=> $lines_read );
189 push @errorloop, {'barcode'=>'No valid barcodes!'};
190 $op=''; # force the initial inventory screen again.
193 $template->param( err_length
=> $err_length,
194 err_data
=> $err_data );
196 foreach my $barcode (@barcodes) {
197 if ( $qwithdrawn->execute($barcode) && $qwithdrawn->rows ) {
198 push @errorloop, { 'barcode' => $barcode, 'ERR_WTHDRAWN' => 1 };
200 my $item = Koha
::Items
->find({barcode
=> $barcode});
202 $item = $item->unblessed;
203 # Modify date last seen for scanned items, remove lost status
204 ModItem
( { itemlost
=> 0, datelastseen
=> $date }, undef, $item->{'itemnumber'} );
206 # update item hash accordingly
207 $item->{itemlost
} = 0;
208 $item->{datelastseen
} = $date;
209 unless ( $dont_checkin ) {
210 $qonloan->execute($barcode);
212 my $data = $qonloan->fetchrow_hashref;
213 my ($doreturn, $messages, $iteminformation, $borrower) =AddReturn
($barcode, $data->{homebranch
});
215 $item->{onloan
} = undef;
216 $item->{datelastseen
} = dt_from_string
;
218 push @errorloop, { barcode
=> $barcode, ERR_ONLOAN_NOT_RET
=> 1 };
222 push @scanned_items, $item;
224 push @errorloop, { barcode
=> $barcode, ERR_BARCODE
=> 1 };
228 $template->param( date
=> $date );
229 $template->param( errorloop
=> \
@errorloop ) if (@errorloop);
232 # Build inventorylist: used as result list when you do not pass barcodes
233 # This list is also used when you want to compare with barcodes
234 my ( $inventorylist, $rightplacelist );
235 if ( $op && ( !$uploadbarcodes || $compareinv2barcd )) {
236 ( $inventorylist ) = GetItemsForInventory
({
237 minlocation
=> $minlocation,
238 maxlocation
=> $maxlocation,
239 class_source
=> $class_source,
240 location
=> $location,
241 ignoreissued
=> $ignoreissued,
242 datelastseen
=> $datelastseen,
243 branchcode
=> $branchcode,
246 statushash
=> $staton,
247 ignore_waiting_holds
=> $ignore_waiting_holds,
250 # Build rightplacelist used to check if a scanned item is in the right place.
251 if( @scanned_items ) {
252 ( $rightplacelist ) = GetItemsForInventory
({
253 minlocation
=> $minlocation,
254 maxlocation
=> $maxlocation,
255 class_source
=> $class_source,
256 location
=> $location,
257 ignoreissued
=> undef,
258 datelastseen
=> undef,
259 branchcode
=> $branchcode,
263 ignore_waiting_holds
=> $ignore_waiting_holds,
265 # Convert the structure to a hash on barcode
267 map { $_->{barcode
} ?
( $_->{barcode
}, $_ ) : (); } @
$rightplacelist
271 # Report scanned items that are on the wrong place, or have a wrong notforloan
272 # status, or are still checked out.
273 for ( my $i = 0; $i < @scanned_items; $i++ ) {
275 my $item = $scanned_items[$i];
277 $item->{notforloancode
} = $item->{notforloan
}; # save for later use
278 my $fc = $item->{'frameworkcode'} || '';
280 # Populating with authorised values description
281 foreach my $field (qw
/ location notforloan itemlost damaged withdrawn /) {
282 my $av = Koha
::AuthorisedValues
->get_description_by_koha_field(
283 { frameworkcode
=> $fc, kohafield
=> "items.$field", authorised_value
=> $item->{$field} } );
284 if ( $av and defined $item->{$field} and defined $av->{lib
} ) {
285 $item->{$field} = $av->{lib
};
289 # If we have scanned items with a non-matching notforloan value
290 if( none
{ $item->{'notforloancode'} eq $_ } @notforloans ) {
291 $item->{problems
}->{changestatus
} = 1;
292 additemtoresults
( $item, $results );
295 # Check for items shelved out of order
298 my $previous_item = $scanned_items[ $i - 1 ];
299 if ( $previous_item && $item->{cn_sort
} lt $previous_item->{cn_sort
} ) {
300 $item->{problems
}->{out_of_order
} = 1;
301 additemtoresults
( $item, $results );
304 unless ( $i == scalar(@scanned_items) ) {
305 my $next_item = $scanned_items[ $i + 1 ];
306 if ( $next_item && $item->{cn_sort
} gt $next_item->{cn_sort
} ) {
307 $item->{problems
}->{out_of_order
} = 1;
308 additemtoresults
( $item, $results );
313 # Report an item that is checked out (unusual!) or wrongly placed
314 if( $item->{onloan
} ) {
315 $item->{problems
}->{checkedout
} = 1;
316 additemtoresults
( $item, $results );
317 next; # do not modify item
318 } elsif( !exists $rightplacelist->{ $item->{barcode
} } ) {
319 $item->{problems
}->{wrongplace
} = 1;
320 additemtoresults
( $item, $results );
324 # Compare barcodes with inventory list, report no_barcode and not_scanned.
325 # not_scanned can be interpreted as missing
326 if ( $compareinv2barcd ) {
327 my @scanned_barcodes = map {$_->{barcode
}} @scanned_items;
328 for my $item ( @
$inventorylist ) {
329 my $barcode = $item->{barcode
};
331 $item->{problems
}->{no_barcode
} = 1;
332 } elsif ( grep { $_ eq $barcode } @scanned_barcodes ) {
335 $item->{problems
}->{not_scanned
} = 1;
337 additemtoresults
( $item, $results );
341 # Construct final results, add biblio information
342 my $loop = $uploadbarcodes
343 ?
[ map { $results->{$_} } keys %$results ]
344 : $inventorylist // [];
345 for my $item ( @
$loop ) {
346 my $biblio = Koha
::Biblios
->find( $item->{biblionumber
} );
347 $item->{title
} = $biblio->title;
348 $item->{author
} = $biblio->author;
352 moddatecount
=> $moddatecount,
358 if (defined $input->param('CSVexport') && $input->param('CSVexport') eq 'on'){
359 eval {use Text
::CSV
};
360 my $csv = Text
::CSV
->new or
361 die Text
::CSV
->error_diag ();
362 binmode STDOUT
, ":encoding(UTF-8)";
363 print $input->header(
365 -attachment
=> 'inventory.csv',
368 my $columns_def_hashref = C4
::Reports
::Guided
::_get_column_defs
($input);
369 foreach my $key ( keys %$columns_def_hashref ) {
371 $key =~ s/[^\.]*\.//;
372 $columns_def_hashref->{$initkey}=NormalizeString
($columns_def_hashref->{$initkey} // '');
373 $columns_def_hashref->{$key} = $columns_def_hashref->{$initkey};
377 for my $key (qw
/ biblioitems
.title biblio
.author
378 items
.barcode items
.itemnumber
379 items
.homebranch items
.location
380 items
.itemcallnumber items
.notforloan
381 items
.itemlost items
.damaged
382 items
.withdrawn items
.stocknumber
384 push @translated_keys, $columns_def_hashref->{$key};
386 push @translated_keys, 'problem' if $uploadbarcodes;
388 $csv->combine(@translated_keys);
389 print $csv->string, "\n";
391 my @keys = qw
/ title author barcode itemnumber homebranch location itemcallnumber notforloan itemlost damaged withdrawn stocknumber /;
392 for my $item ( @
$loop ) {
394 for my $key (@keys) {
395 push @line, $item->{$key};
398 foreach my $key ( keys %{$item->{problems
}} ) {
399 if( $key eq 'wrongplace' ) {
400 $errstr .= "wrong place,";
401 } elsif( $key eq 'changestatus' ) {
402 $errstr .= "unknown notforloan status,";
403 } elsif( $key eq 'not_scanned' ) {
404 $errstr .= "missing,";
405 } elsif( $key eq 'no_barcode' ) {
406 $errstr .= "no barcode,";
407 } elsif( $key eq 'checkedout' ) {
408 $errstr .= "checked out,";
413 $csv->combine(@line);
414 print $csv->string, "\n";
416 # Adding not found barcodes
417 foreach my $error (@errorloop) {
419 if ($error->{'ERR_BARCODE'}) {
420 push @line, map { $_ eq 'barcode' ?
$error->{'barcode'} : ''} @keys;
421 push @line, "barcode not found";
422 $csv->combine(@line);
423 print $csv->string, "\n";
429 output_html_with_http_headers
$input, $cookie, $template->output;
431 sub additemtoresults
{
432 my ( $item, $results ) = @_;
433 my $itemno = $item->{itemnumber
};
434 # since the script appends to $item, we can just overwrite the hash entry
435 $results->{$itemno} = $item;