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');
27 my $barcodelist = $input->param('barcodelist');
36 use C4
::Reports
::Guided
; #_get_column_defs
41 use Koha
::AuthorisedValues
;
42 use Koha
::BiblioFrameworks
;
43 use Koha
::ClassSources
;
45 use List
::MoreUtils
qw( none );
47 my $minlocation=$input->param('minlocation') || '';
48 my $maxlocation=$input->param('maxlocation');
49 my $class_source=$input->param('class_source');
50 $maxlocation=$minlocation.'Z' unless ( $maxlocation || ! $minlocation );
51 my $location=$input->param('location') || '';
52 my $ignoreissued=$input->param('ignoreissued');
53 my $ignore_waiting_holds = $input->param('ignore_waiting_holds');
54 my $datelastseen = $input->param('datelastseen'); # last inventory date
55 my $branchcode = $input->param('branchcode') || '';
56 my $branch = $input->param('branch');
57 my $op = $input->param('op');
58 my $compareinv2barcd = $input->param('compareinv2barcd');
59 my $dont_checkin = $input->param('dont_checkin');
60 my $out_of_order = $input->param('out_of_order');
62 my ( $template, $borrowernumber, $cookie ) = get_template_and_user
(
63 { 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 # if there's a list of not for loans types selected use it rather than
124 @notforloans = @
{$staton->{'items.notforloan'}} if defined $staton->{'items.notforloan'} and scalar @
{$staton->{'items.notforloan'}} > 0;
126 my @class_sources = Koha
::ClassSources
->search({ used
=> 1 });
127 my $pref_class = C4
::Context
->preference("DefaultClassificationSource");
131 authorised_values
=> \
@authorised_value_list,
132 today
=> dt_from_string
,
133 minlocation
=> $minlocation,
134 maxlocation
=> $maxlocation,
135 location
=> $location,
136 ignoreissued
=> $ignoreissued,
137 branchcode
=> $branchcode,
139 datelastseen
=> $datelastseen,
140 compareinv2barcd
=> $compareinv2barcd,
141 uploadedbarcodesflag
=> ($uploadbarcodes || $barcodelist) ?
1 : 0,
142 ignore_waiting_holds
=> $ignore_waiting_holds,
143 class_sources
=> \
@class_sources,
144 pref_class
=> $pref_class
147 # Walk through uploaded barcodes, report errors, mark as seen, check in
151 my $moddatecount = 0;
152 if ( ($uploadbarcodes && length($uploadbarcodes) > 0) || ($barcodelist && length($barcodelist) > 0) ) {
153 my $dbh = C4
::Context
->dbh;
154 my $date = dt_from_string
( scalar $input->param('setdate') );
155 $date = output_pref
( { dt
=> $date, dateformat
=> 'iso' } );
157 my $strsth = "select * from issues, items where items.itemnumber=issues.itemnumber and items.barcode =?";
158 my $qonloan = $dbh->prepare($strsth);
159 $strsth="select * from items where items.barcode =? and items.withdrawn = 1";
160 my $qwithdrawn = $dbh->prepare($strsth);
163 my @uploadedbarcodes;
165 my $sth = $dbh->column_info(undef,undef,"items","barcode");
166 my $barcode_def = $sth->fetchall_hashref('COLUMN_NAME');
167 my $barcode_size = $barcode_def->{barcode
}->{COLUMN_SIZE
};
171 if ($uploadbarcodes && length($uploadbarcodes) > 0) {
172 binmode($uploadbarcodes, ":encoding(UTF-8)");
173 while (my $barcode=<$uploadbarcodes>) {
174 my $split_chars = C4
::Context
->preference('BarcodeSeparators');
175 push @uploadedbarcodes, grep { /\S/ } split( /[$split_chars]/, $barcode );
178 push @uploadedbarcodes, split(/\s\n/, $input->param('barcodelist') );
179 $uploadbarcodes = $barcodelist;
181 for my $barcode (@uploadedbarcodes) {
182 next unless $barcode;
184 if (length($barcode)>$barcode_size) {
187 my $check_barcode = $barcode;
188 $check_barcode =~ s/\p{Print}//g;
189 if (length($check_barcode)>0) { # Only printable unicode characters allowed.
192 next if length($barcode)>$barcode_size;
193 next if ( length($check_barcode)>0 );
194 push @barcodes,$barcode;
196 $template->param( LinesRead
=> $lines_read );
198 push @errorloop, {'barcode'=>'No valid barcodes!'};
199 $op=''; # force the initial inventory screen again.
202 $template->param( err_length
=> $err_length,
203 err_data
=> $err_data );
205 foreach my $barcode (@barcodes) {
206 if ( $qwithdrawn->execute($barcode) && $qwithdrawn->rows ) {
207 push @errorloop, { 'barcode' => $barcode, 'ERR_WTHDRAWN' => 1 };
209 my $item = Koha
::Items
->find({barcode
=> $barcode});
211 # Modify date last seen for scanned items, remove lost status
212 $item->set({ itemlost
=> 0, datelastseen
=> $date })->store;
213 my $item_unblessed = $item->unblessed;
215 unless ( $dont_checkin ) {
216 $qonloan->execute($barcode);
218 my $data = $qonloan->fetchrow_hashref;
219 my ($doreturn, $messages, $iteminformation, $borrower) =AddReturn
($barcode, $data->{homebranch
});
221 $item_unblessed->{onloan
} = undef;
222 $item_unblessed->{datelastseen
} = dt_from_string
;
224 push @errorloop, { barcode
=> $barcode, ERR_ONLOAN_NOT_RET
=> 1 };
228 push @scanned_items, $item_unblessed;
230 push @errorloop, { barcode
=> $barcode, ERR_BARCODE
=> 1 };
234 $template->param( date
=> $date );
235 $template->param( errorloop
=> \
@errorloop ) if (@errorloop);
238 # Build inventorylist: used as result list when you do not pass barcodes
239 # This list is also used when you want to compare with barcodes
240 my ( $inventorylist, $rightplacelist );
241 if ( $op && ( !$uploadbarcodes || $compareinv2barcd )) {
242 ( $inventorylist ) = GetItemsForInventory
({
243 minlocation
=> $minlocation,
244 maxlocation
=> $maxlocation,
245 class_source
=> $class_source,
246 location
=> $location,
247 ignoreissued
=> $ignoreissued,
248 datelastseen
=> $datelastseen,
249 branchcode
=> $branchcode,
252 statushash
=> $staton,
253 ignore_waiting_holds
=> $ignore_waiting_holds,
256 # Build rightplacelist used to check if a scanned item is in the right place.
257 if( @scanned_items ) {
258 ( $rightplacelist ) = GetItemsForInventory
({
259 minlocation
=> $minlocation,
260 maxlocation
=> $maxlocation,
261 class_source
=> $class_source,
262 location
=> $location,
263 ignoreissued
=> undef,
264 datelastseen
=> undef,
265 branchcode
=> $branchcode,
269 ignore_waiting_holds
=> $ignore_waiting_holds,
271 # Convert the structure to a hash on barcode
273 map { $_->{barcode
} ?
( $_->{barcode
}, $_ ) : (); } @
$rightplacelist
277 # Report scanned items that are on the wrong place, or have a wrong notforloan
278 # status, or are still checked out.
279 for ( my $i = 0; $i < @scanned_items; $i++ ) {
281 my $item = $scanned_items[$i];
283 $item->{notforloancode
} = $item->{notforloan
}; # save for later use
284 my $fc = $item->{'frameworkcode'} || '';
286 # Populating with authorised values description
287 foreach my $field (qw
/ location notforloan itemlost damaged withdrawn /) {
288 my $av = Koha
::AuthorisedValues
->get_description_by_koha_field(
289 { frameworkcode
=> $fc, kohafield
=> "items.$field", authorised_value
=> $item->{$field} } );
290 if ( $av and defined $item->{$field} and defined $av->{lib
} ) {
291 $item->{$field} = $av->{lib
};
295 # If we have scanned items with a non-matching notforloan value
296 if( none
{ $item->{'notforloancode'} eq $_ } @notforloans ) {
297 $item->{problems
}->{changestatus
} = 1;
298 additemtoresults
( $item, $results );
301 # Check for items shelved out of order
304 my $previous_item = $scanned_items[ $i - 1 ];
305 if ( $previous_item && $item->{cn_sort
} lt $previous_item->{cn_sort
} ) {
306 $item->{problems
}->{out_of_order
} = 1;
307 additemtoresults
( $item, $results );
310 unless ( $i == scalar(@scanned_items) ) {
311 my $next_item = $scanned_items[ $i + 1 ];
312 if ( $next_item && $item->{cn_sort
} gt $next_item->{cn_sort
} ) {
313 $item->{problems
}->{out_of_order
} = 1;
314 additemtoresults
( $item, $results );
319 # Report an item that is checked out (unusual!) or wrongly placed
320 if( $item->{onloan
} ) {
321 $item->{problems
}->{checkedout
} = 1;
322 additemtoresults
( $item, $results );
323 next; # do not modify item
324 } elsif( !exists $rightplacelist->{ $item->{barcode
} } ) {
325 $item->{problems
}->{wrongplace
} = 1;
326 additemtoresults
( $item, $results );
330 # Compare barcodes with inventory list, report no_barcode and not_scanned.
331 # not_scanned can be interpreted as missing
332 if ( $compareinv2barcd ) {
333 my @scanned_barcodes = map {$_->{barcode
}} @scanned_items;
334 for my $item ( @
$inventorylist ) {
335 my $barcode = $item->{barcode
};
337 $item->{problems
}->{no_barcode
} = 1;
338 } elsif ( grep { $_ eq $barcode } @scanned_barcodes ) {
341 $item->{problems
}->{not_scanned
} = 1;
343 additemtoresults
( $item, $results );
347 # Construct final results, add biblio information
348 my $loop = $uploadbarcodes
349 ?
[ map { $results->{$_} } keys %$results ]
350 : $inventorylist // [];
351 for my $item ( @
$loop ) {
352 my $biblio = Koha
::Biblios
->find( $item->{biblionumber
} );
353 $item->{title
} = $biblio->title;
354 $item->{author
} = $biblio->author;
358 moddatecount
=> $moddatecount,
364 if (defined $input->param('CSVexport') && $input->param('CSVexport') eq 'on'){
365 eval {use Text
::CSV
};
366 my $csv = Text
::CSV
->new or
367 die Text
::CSV
->error_diag ();
368 binmode STDOUT
, ":encoding(UTF-8)";
369 print $input->header(
371 -attachment
=> 'inventory.csv',
374 my $columns_def_hashref = C4
::Reports
::Guided
::_get_column_defs
($input);
375 foreach my $key ( keys %$columns_def_hashref ) {
377 $key =~ s/[^\.]*\.//;
378 $columns_def_hashref->{$initkey}=NormalizeString
($columns_def_hashref->{$initkey} // '');
379 $columns_def_hashref->{$key} = $columns_def_hashref->{$initkey};
383 for my $key (qw
/ biblioitems
.title biblio
.author
384 items
.barcode items
.itemnumber
385 items
.homebranch items
.location
386 items
.itemcallnumber items
.notforloan
387 items
.itemlost items
.damaged
388 items
.withdrawn items
.stocknumber
390 push @translated_keys, $columns_def_hashref->{$key};
392 push @translated_keys, 'problem' if $uploadbarcodes;
394 $csv->combine(@translated_keys);
395 print $csv->string, "\n";
397 my @keys = qw
/ title author barcode itemnumber homebranch location itemcallnumber notforloan itemlost damaged withdrawn stocknumber /;
398 for my $item ( @
$loop ) {
400 for my $key (@keys) {
401 push @line, $item->{$key};
404 foreach my $key ( keys %{$item->{problems
}} ) {
405 if( $key eq 'wrongplace' ) {
406 $errstr .= "wrong place,";
407 } elsif( $key eq 'changestatus' ) {
408 $errstr .= "unknown notforloan status,";
409 } elsif( $key eq 'not_scanned' ) {
410 $errstr .= "missing,";
411 } elsif( $key eq 'no_barcode' ) {
412 $errstr .= "no barcode,";
413 } elsif( $key eq 'checkedout' ) {
414 $errstr .= "checked out,";
419 $csv->combine(@line);
420 print $csv->string, "\n";
422 # Adding not found barcodes
423 foreach my $error (@errorloop) {
425 if ($error->{'ERR_BARCODE'}) {
426 push @line, map { $_ eq 'barcode' ?
$error->{'barcode'} : ''} @keys;
427 push @line, "barcode not found";
428 $csv->combine(@line);
429 print $csv->string, "\n";
435 output_html_with_http_headers
$input, $cookie, $template->output;
437 sub additemtoresults
{
438 my ( $item, $results ) = @_;
439 my $itemno = $item->{itemnumber
};
440 # since the script appends to $item, we can just overwrite the hash entry
441 $results->{$itemno} = $item;