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 push @uploadedbarcodes, grep { /\S/ } split( /[\n\r,;|-]/, $barcode );
171 for my $barcode (@uploadedbarcodes) {
172 next unless $barcode;
174 if (length($barcode)>$barcode_size) {
177 my $check_barcode = $barcode;
178 $check_barcode =~ s/\p{Print}//g;
179 if (length($check_barcode)>0) { # Only printable unicode characters allowed.
182 next if length($barcode)>$barcode_size;
183 next if ( length($check_barcode)>0 );
184 push @barcodes,$barcode;
186 $template->param( LinesRead
=> $lines_read );
188 push @errorloop, {'barcode'=>'No valid barcodes!'};
189 $op=''; # force the initial inventory screen again.
192 $template->param( err_length
=> $err_length,
193 err_data
=> $err_data );
195 foreach my $barcode (@barcodes) {
196 if ( $qwithdrawn->execute($barcode) && $qwithdrawn->rows ) {
197 push @errorloop, { 'barcode' => $barcode, 'ERR_WTHDRAWN' => 1 };
199 my $item = Koha
::Items
->find({barcode
=> $barcode});
201 $item = $item->unblessed;
202 # Modify date last seen for scanned items, remove lost status
203 ModItem
( { itemlost
=> 0, datelastseen
=> $date }, undef, $item->{'itemnumber'} );
205 # update item hash accordingly
206 $item->{itemlost
} = 0;
207 $item->{datelastseen
} = $date;
208 unless ( $dont_checkin ) {
209 $qonloan->execute($barcode);
211 my $data = $qonloan->fetchrow_hashref;
212 my ($doreturn, $messages, $iteminformation, $borrower) =AddReturn
($barcode, $data->{homebranch
});
214 $item->{onloan
} = undef;
215 $item->{datelastseen
} = dt_from_string
;
217 push @errorloop, { barcode
=> $barcode, ERR_ONLOAN_NOT_RET
=> 1 };
221 push @scanned_items, $item;
223 push @errorloop, { barcode
=> $barcode, ERR_BARCODE
=> 1 };
227 $template->param( date
=> $date );
228 $template->param( errorloop
=> \
@errorloop ) if (@errorloop);
231 # Build inventorylist: used as result list when you do not pass barcodes
232 # This list is also used when you want to compare with barcodes
233 my ( $inventorylist, $rightplacelist );
234 if ( $op && ( !$uploadbarcodes || $compareinv2barcd )) {
235 ( $inventorylist ) = GetItemsForInventory
({
236 minlocation
=> $minlocation,
237 maxlocation
=> $maxlocation,
238 class_source
=> $class_source,
239 location
=> $location,
240 ignoreissued
=> $ignoreissued,
241 datelastseen
=> $datelastseen,
242 branchcode
=> $branchcode,
245 statushash
=> $staton,
246 ignore_waiting_holds
=> $ignore_waiting_holds,
249 # Build rightplacelist used to check if a scanned item is in the right place.
250 if( @scanned_items ) {
251 ( $rightplacelist ) = GetItemsForInventory
({
252 minlocation
=> $minlocation,
253 maxlocation
=> $maxlocation,
254 class_source
=> $class_source,
255 location
=> $location,
256 ignoreissued
=> undef,
257 datelastseen
=> undef,
258 branchcode
=> $branchcode,
262 ignore_waiting_holds
=> $ignore_waiting_holds,
264 # Convert the structure to a hash on barcode
266 map { $_->{barcode
} ?
( $_->{barcode
}, $_ ) : (); } @
$rightplacelist
270 # Report scanned items that are on the wrong place, or have a wrong notforloan
271 # status, or are still checked out.
272 for ( my $i = 0; $i < @scanned_items; $i++ ) {
274 my $item = $scanned_items[$i];
276 $item->{notforloancode
} = $item->{notforloan
}; # save for later use
277 my $fc = $item->{'frameworkcode'} || '';
279 # Populating with authorised values description
280 foreach my $field (qw
/ location notforloan itemlost damaged withdrawn /) {
281 my $av = Koha
::AuthorisedValues
->get_description_by_koha_field(
282 { frameworkcode
=> $fc, kohafield
=> "items.$field", authorised_value
=> $item->{$field} } );
283 if ( $av and defined $item->{$field} and defined $av->{lib
} ) {
284 $item->{$field} = $av->{lib
};
288 # If we have scanned items with a non-matching notforloan value
289 if( none
{ $item->{'notforloancode'} eq $_ } @notforloans ) {
290 $item->{problems
}->{changestatus
} = 1;
291 additemtoresults
( $item, $results );
294 # Check for items shelved out of order
297 my $previous_item = $scanned_items[ $i - 1 ];
298 if ( $previous_item && $item->{cn_sort
} lt $previous_item->{cn_sort
} ) {
299 $item->{problems
}->{out_of_order
} = 1;
300 additemtoresults
( $item, $results );
303 unless ( $i == scalar(@scanned_items) ) {
304 my $next_item = $scanned_items[ $i + 1 ];
305 if ( $next_item && $item->{cn_sort
} gt $next_item->{cn_sort
} ) {
306 $item->{problems
}->{out_of_order
} = 1;
307 additemtoresults
( $item, $results );
312 # Report an item that is checked out (unusual!) or wrongly placed
313 if( $item->{onloan
} ) {
314 $item->{problems
}->{checkedout
} = 1;
315 additemtoresults
( $item, $results );
316 next; # do not modify item
317 } elsif( !exists $rightplacelist->{ $item->{barcode
} } ) {
318 $item->{problems
}->{wrongplace
} = 1;
319 additemtoresults
( $item, $results );
323 # Compare barcodes with inventory list, report no_barcode and not_scanned.
324 # not_scanned can be interpreted as missing
325 if ( $compareinv2barcd ) {
326 my @scanned_barcodes = map {$_->{barcode
}} @scanned_items;
327 for my $item ( @
$inventorylist ) {
328 my $barcode = $item->{barcode
};
330 $item->{problems
}->{no_barcode
} = 1;
331 } elsif ( grep /^$barcode$/, @scanned_barcodes ) {
334 $item->{problems
}->{not_scanned
} = 1;
336 additemtoresults
( $item, $results );
340 # Construct final results, add biblio information
341 my $loop = $uploadbarcodes
342 ?
[ map { $results->{$_} } keys %$results ]
343 : $inventorylist // [];
344 for my $item ( @
$loop ) {
345 my $biblio = Koha
::Biblios
->find( $item->{biblionumber
} );
346 $item->{title
} = $biblio->title;
347 $item->{author
} = $biblio->author;
351 moddatecount
=> $moddatecount,
357 if (defined $input->param('CSVexport') && $input->param('CSVexport') eq 'on'){
358 eval {use Text
::CSV
};
359 my $csv = Text
::CSV
->new or
360 die Text
::CSV
->error_diag ();
361 binmode STDOUT
, ":encoding(UTF-8)";
362 print $input->header(
364 -attachment
=> 'inventory.csv',
367 my $columns_def_hashref = C4
::Reports
::Guided
::_get_column_defs
($input);
368 foreach my $key ( keys %$columns_def_hashref ) {
370 $key =~ s/[^\.]*\.//;
371 $columns_def_hashref->{$initkey}=NormalizeString
($columns_def_hashref->{$initkey} // '');
372 $columns_def_hashref->{$key} = $columns_def_hashref->{$initkey};
376 for my $key (qw
/ biblioitems
.title biblio
.author
377 items
.barcode items
.itemnumber
378 items
.homebranch items
.location
379 items
.itemcallnumber items
.notforloan
380 items
.itemlost items
.damaged
381 items
.withdrawn items
.stocknumber
383 push @translated_keys, $columns_def_hashref->{$key};
385 push @translated_keys, 'problem' if $uploadbarcodes;
387 $csv->combine(@translated_keys);
388 print $csv->string, "\n";
390 my @keys = qw
/ title author barcode itemnumber homebranch location itemcallnumber notforloan itemlost damaged withdrawn stocknumber /;
391 for my $item ( @
$loop ) {
393 for my $key (@keys) {
394 push @line, $item->{$key};
397 foreach my $key ( keys %{$item->{problems
}} ) {
398 if( $key eq 'wrongplace' ) {
399 $errstr .= "wrong place,";
400 } elsif( $key eq 'changestatus' ) {
401 $errstr .= "unknown notforloan status,";
402 } elsif( $key eq 'not_scanned' ) {
403 $errstr .= "missing,";
404 } elsif( $key eq 'no_barcode' ) {
405 $errstr .= "no barcode,";
406 } elsif( $key eq 'checkedout' ) {
407 $errstr .= "checked out,";
412 $csv->combine(@line);
413 print $csv->string, "\n";
415 # Adding not found barcodes
416 foreach my $error (@errorloop) {
418 if ($error->{'ERR_BARCODE'}) {
419 push @line, map { $_ eq 'barcode' ?
$error->{'barcode'} : ''} @keys;
420 push @line, "barcode not found";
421 $csv->combine(@line);
422 print $csv->string, "\n";
428 output_html_with_http_headers
$input, $cookie, $template->output;
430 sub additemtoresults
{
431 my ( $item, $results ) = @_;
432 my $itemno = $item->{itemnumber
};
433 # since the script appends to $item, we can just overwrite the hash entry
434 $results->{$itemno} = $item;