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 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 || $barcodelist) ?
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) || ($barcodelist && length($barcodelist) > 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 if ($uploadbarcodes && length($uploadbarcodes) > 0) {
168 binmode($uploadbarcodes, ":encoding(UTF-8)");
169 while (my $barcode=<$uploadbarcodes>) {
170 my $split_chars = C4
::Context
->preference('BarcodeSeparators');
171 push @uploadedbarcodes, grep { /\S/ } split( /[$split_chars]/, $barcode );
174 push @uploadedbarcodes, split(/\s\n/, $input->param('barcodelist') );
175 $uploadbarcodes = $barcodelist;
177 for my $barcode (@uploadedbarcodes) {
178 next unless $barcode;
180 if (length($barcode)>$barcode_size) {
183 my $check_barcode = $barcode;
184 $check_barcode =~ s/\p{Print}//g;
185 if (length($check_barcode)>0) { # Only printable unicode characters allowed.
188 next if length($barcode)>$barcode_size;
189 next if ( length($check_barcode)>0 );
190 push @barcodes,$barcode;
192 $template->param( LinesRead
=> $lines_read );
194 push @errorloop, {'barcode'=>'No valid barcodes!'};
195 $op=''; # force the initial inventory screen again.
198 $template->param( err_length
=> $err_length,
199 err_data
=> $err_data );
201 foreach my $barcode (@barcodes) {
202 if ( $qwithdrawn->execute($barcode) && $qwithdrawn->rows ) {
203 push @errorloop, { 'barcode' => $barcode, 'ERR_WTHDRAWN' => 1 };
205 my $item = Koha
::Items
->find({barcode
=> $barcode});
207 # Modify date last seen for scanned items, remove lost status
208 $item->set({ itemlost
=> 0, datelastseen
=> $date })->store;
209 my $item_unblessed = $item->unblessed;
211 unless ( $dont_checkin ) {
212 $qonloan->execute($barcode);
214 my $data = $qonloan->fetchrow_hashref;
215 my ($doreturn, $messages, $iteminformation, $borrower) =AddReturn
($barcode, $data->{homebranch
});
217 $item_unblessed->{onloan
} = undef;
218 $item_unblessed->{datelastseen
} = dt_from_string
;
220 push @errorloop, { barcode
=> $barcode, ERR_ONLOAN_NOT_RET
=> 1 };
224 push @scanned_items, $item_unblessed;
226 push @errorloop, { barcode
=> $barcode, ERR_BARCODE
=> 1 };
230 $template->param( date
=> $date );
231 $template->param( errorloop
=> \
@errorloop ) if (@errorloop);
234 # Build inventorylist: used as result list when you do not pass barcodes
235 # This list is also used when you want to compare with barcodes
236 my ( $inventorylist, $rightplacelist );
237 if ( $op && ( !$uploadbarcodes || $compareinv2barcd )) {
238 ( $inventorylist ) = GetItemsForInventory
({
239 minlocation
=> $minlocation,
240 maxlocation
=> $maxlocation,
241 class_source
=> $class_source,
242 location
=> $location,
243 ignoreissued
=> $ignoreissued,
244 datelastseen
=> $datelastseen,
245 branchcode
=> $branchcode,
248 statushash
=> $staton,
249 ignore_waiting_holds
=> $ignore_waiting_holds,
252 # Build rightplacelist used to check if a scanned item is in the right place.
253 if( @scanned_items ) {
254 ( $rightplacelist ) = GetItemsForInventory
({
255 minlocation
=> $minlocation,
256 maxlocation
=> $maxlocation,
257 class_source
=> $class_source,
258 location
=> $location,
259 ignoreissued
=> undef,
260 datelastseen
=> undef,
261 branchcode
=> $branchcode,
265 ignore_waiting_holds
=> $ignore_waiting_holds,
267 # Convert the structure to a hash on barcode
269 map { $_->{barcode
} ?
( $_->{barcode
}, $_ ) : (); } @
$rightplacelist
273 # Report scanned items that are on the wrong place, or have a wrong notforloan
274 # status, or are still checked out.
275 for ( my $i = 0; $i < @scanned_items; $i++ ) {
277 my $item = $scanned_items[$i];
279 $item->{notforloancode
} = $item->{notforloan
}; # save for later use
280 my $fc = $item->{'frameworkcode'} || '';
282 # Populating with authorised values description
283 foreach my $field (qw
/ location notforloan itemlost damaged withdrawn /) {
284 my $av = Koha
::AuthorisedValues
->get_description_by_koha_field(
285 { frameworkcode
=> $fc, kohafield
=> "items.$field", authorised_value
=> $item->{$field} } );
286 if ( $av and defined $item->{$field} and defined $av->{lib
} ) {
287 $item->{$field} = $av->{lib
};
291 # If we have scanned items with a non-matching notforloan value
292 if( none
{ $item->{'notforloancode'} eq $_ } @notforloans ) {
293 $item->{problems
}->{changestatus
} = 1;
294 additemtoresults
( $item, $results );
297 # Check for items shelved out of order
300 my $previous_item = $scanned_items[ $i - 1 ];
301 if ( $previous_item && $item->{cn_sort
} lt $previous_item->{cn_sort
} ) {
302 $item->{problems
}->{out_of_order
} = 1;
303 additemtoresults
( $item, $results );
306 unless ( $i == scalar(@scanned_items) ) {
307 my $next_item = $scanned_items[ $i + 1 ];
308 if ( $next_item && $item->{cn_sort
} gt $next_item->{cn_sort
} ) {
309 $item->{problems
}->{out_of_order
} = 1;
310 additemtoresults
( $item, $results );
315 # Report an item that is checked out (unusual!) or wrongly placed
316 if( $item->{onloan
} ) {
317 $item->{problems
}->{checkedout
} = 1;
318 additemtoresults
( $item, $results );
319 next; # do not modify item
320 } elsif( !exists $rightplacelist->{ $item->{barcode
} } ) {
321 $item->{problems
}->{wrongplace
} = 1;
322 additemtoresults
( $item, $results );
326 # Compare barcodes with inventory list, report no_barcode and not_scanned.
327 # not_scanned can be interpreted as missing
328 if ( $compareinv2barcd ) {
329 my @scanned_barcodes = map {$_->{barcode
}} @scanned_items;
330 for my $item ( @
$inventorylist ) {
331 my $barcode = $item->{barcode
};
333 $item->{problems
}->{no_barcode
} = 1;
334 } elsif ( grep { $_ eq $barcode } @scanned_barcodes ) {
337 $item->{problems
}->{not_scanned
} = 1;
339 additemtoresults
( $item, $results );
343 # Construct final results, add biblio information
344 my $loop = $uploadbarcodes
345 ?
[ map { $results->{$_} } keys %$results ]
346 : $inventorylist // [];
347 for my $item ( @
$loop ) {
348 my $biblio = Koha
::Biblios
->find( $item->{biblionumber
} );
349 $item->{title
} = $biblio->title;
350 $item->{author
} = $biblio->author;
354 moddatecount
=> $moddatecount,
360 if (defined $input->param('CSVexport') && $input->param('CSVexport') eq 'on'){
361 eval {use Text
::CSV
};
362 my $csv = Text
::CSV
->new or
363 die Text
::CSV
->error_diag ();
364 binmode STDOUT
, ":encoding(UTF-8)";
365 print $input->header(
367 -attachment
=> 'inventory.csv',
370 my $columns_def_hashref = C4
::Reports
::Guided
::_get_column_defs
($input);
371 foreach my $key ( keys %$columns_def_hashref ) {
373 $key =~ s/[^\.]*\.//;
374 $columns_def_hashref->{$initkey}=NormalizeString
($columns_def_hashref->{$initkey} // '');
375 $columns_def_hashref->{$key} = $columns_def_hashref->{$initkey};
379 for my $key (qw
/ biblioitems
.title biblio
.author
380 items
.barcode items
.itemnumber
381 items
.homebranch items
.location
382 items
.itemcallnumber items
.notforloan
383 items
.itemlost items
.damaged
384 items
.withdrawn items
.stocknumber
386 push @translated_keys, $columns_def_hashref->{$key};
388 push @translated_keys, 'problem' if $uploadbarcodes;
390 $csv->combine(@translated_keys);
391 print $csv->string, "\n";
393 my @keys = qw
/ title author barcode itemnumber homebranch location itemcallnumber notforloan itemlost damaged withdrawn stocknumber /;
394 for my $item ( @
$loop ) {
396 for my $key (@keys) {
397 push @line, $item->{$key};
400 foreach my $key ( keys %{$item->{problems
}} ) {
401 if( $key eq 'wrongplace' ) {
402 $errstr .= "wrong place,";
403 } elsif( $key eq 'changestatus' ) {
404 $errstr .= "unknown notforloan status,";
405 } elsif( $key eq 'not_scanned' ) {
406 $errstr .= "missing,";
407 } elsif( $key eq 'no_barcode' ) {
408 $errstr .= "no barcode,";
409 } elsif( $key eq 'checkedout' ) {
410 $errstr .= "checked out,";
415 $csv->combine(@line);
416 print $csv->string, "\n";
418 # Adding not found barcodes
419 foreach my $error (@errorloop) {
421 if ($error->{'ERR_BARCODE'}) {
422 push @line, map { $_ eq 'barcode' ?
$error->{'barcode'} : ''} @keys;
423 push @line, "barcode not found";
424 $csv->combine(@line);
425 print $csv->string, "\n";
431 output_html_with_http_headers
$input, $cookie, $template->output;
433 sub additemtoresults
{
434 my ( $item, $results ) = @_;
435 my $itemno = $item->{itemnumber
};
436 # since the script appends to $item, we can just overwrite the hash entry
437 $results->{$itemno} = $item;