Koha/tools/inventory.pl
Mark Tompsett d75a751d49 Bug 10625: Inventory/Stocktaking tool cannot handle windows file uploads
The current code uses
    $barcode = <fh>;
logic. This reads until \n, as far as I can tell.
EOL is indicated by \n, \r, and \r\n depending on OS and software.
So, to this end, rather than File::Slurp (which is a potential
memory hog, which is already an issue with no filters), a loop
to pre-read the barcodes was written.

This loop includes:
    $barcode =~ s/\r/\n/g;
    $barcode =~ s/\n\n/\n/g;
    my @data = split(/\n/, $barcode);
    push @uploadedbarcodes,@data;

So, that means that lines ending in \n would have it stripped
and pushed into the uploaded barcodes array.

Lines ending in \r would likely be read as one giant block,
have everything converted to single \n's and then using a split,
the set of barcodes are pushed into the uploaded barcodes array.

Lines ending in \r\n would get that stripped and pushed into the
uploaded barcodes array.

It is then the uploaded barcodes array that is looped over for
validating the barcodes.

TEST PLAN
---------
 1) Back up your database
 2) Download the three sample files (or create your own)
 3) Log in to staff client
 4) Create a branch with no inventory.
 5) Home -> Tools -> Inventory/Stocktaking
 6) Browse for your '\r' test file.
 7) Limit to just that branch
 8) Click 'Submit'
    -- Confirm expected errors
 9) Repeat steps 5-8 with the '\n' test file.
10) Repeat steps 5-8 with the '\r\n' test file.
    -- one of these repetitions should have problems.
11) Apply patch
12) Repeat steps 5-8 for each of the 3 test files.
    -- there should be no issues.
13) run koha qa test tools.

Note: This is a tweak based on Jonathan Druart's comment #16
      I have reset it to needs sign off again.

Followed test plan. Works as expected. qa OK.
Signed-off-by: Marc Véron <veron@veron.ch>

Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Tomas Cohen Arazi <tomascohen@gmail.com>
2015-06-11 10:04:04 -03:00

430 lines
15 KiB
Perl
Executable file

#!/usr/bin/perl
# Copyright 2000-2009 Biblibre S.A
# John Soros <john.soros@biblibre.com>
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use strict;
use warnings;
#need to open cgi and get the fh before anything else opens a new cgi context (see C4::Auth)
use CGI qw ( -utf8 );
my $input = CGI->new;
my $uploadbarcodes = $input->param('uploadbarcodes');
use C4::Auth;
use C4::Context;
use C4::Output;
use C4::Biblio;
use C4::Items;
use C4::Koha;
use C4::Branch; # GetBranches
use C4::Circulation;
use C4::Reports::Guided; #_get_column_defs
use C4::Charset;
use Koha::DateUtils;
use List::MoreUtils qw( none );
my $minlocation=$input->param('minlocation') || '';
my $maxlocation=$input->param('maxlocation');
$maxlocation=$minlocation.'Z' unless ( $maxlocation || ! $minlocation );
my $location=$input->param('location') || '';
my $itemtype=$input->param('itemtype'); # FIXME note, template does not currently supply this
my $ignoreissued=$input->param('ignoreissued');
my $datelastseen = $input->param('datelastseen');
my $markseen = $input->param('markseen');
my $branchcode = $input->param('branchcode') || '';
my $branch = $input->param('branch');
my $op = $input->param('op');
my $compareinv2barcd = $input->param('compareinv2barcd');
my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
{ template_name => "tools/inventory.tt",
query => $input,
type => "intranet",
authnotrequired => 0,
flagsrequired => { tools => 'inventory' },
debug => 1,
}
);
my $branches = GetBranches();
my @branch_loop;
for my $branch_hash (keys %$branches) {
push @branch_loop, {value => "$branch_hash",
branchname => $branches->{$branch_hash}->{'branchname'},
selected => ($branch_hash eq $branchcode?1:0)};
}
@branch_loop = sort {$a->{branchname} cmp $b->{branchname}} @branch_loop;
my @authorised_value_list;
my $authorisedvalue_categories = '';
my $frameworks = getframeworks();
$frameworks->{''} = {frameworkcode => ''}; # Add the default framework
for my $fwk (keys %$frameworks){
my $fwkcode = $frameworks->{$fwk}->{'frameworkcode'};
my $authcode = GetAuthValCode('items.location', $fwkcode);
if ($authcode && $authorisedvalue_categories!~/\b$authcode\W/){
$authorisedvalue_categories.="$authcode ";
my $data=GetAuthorisedValues($authcode);
foreach my $value (@$data){
$value->{selected}=1 if ($value->{authorised_value} eq ($location));
}
push @authorised_value_list,@$data;
}
}
my $statuses = [];
for my $statfield (qw/items.notforloan items.itemlost items.withdrawn items.damaged/){
my $hash = {};
$hash->{fieldname} = $statfield;
$hash->{authcode} = GetAuthValCode($statfield);
if ($hash->{authcode}){
my $arr = GetAuthorisedValues($hash->{authcode});
$hash->{values} = $arr;
push @$statuses, $hash;
}
}
$template->param( statuses => $statuses );
my $staton = {}; #authorized values that are ticked
for my $authvfield (@$statuses) {
$staton->{$authvfield->{fieldname}} = [];
for my $authval (@{$authvfield->{values}}){
if ( defined $input->param('status-' . $authvfield->{fieldname} . '-' . $authval->{authorised_value}) && $input->param('status-' . $authvfield->{fieldname} . '-' . $authval->{authorised_value}) eq 'on' ){
push @{$staton->{$authvfield->{fieldname}}}, $authval->{authorised_value};
}
}
}
my $notforloanlist;
my $statussth = '';
for my $authvfield (@$statuses) {
if ( scalar @{$staton->{$authvfield->{fieldname}}} > 0 ){
my $joinedvals = join ',', @{$staton->{$authvfield->{fieldname}}};
$statussth .= "$authvfield->{fieldname} in ($joinedvals) and ";
$notforloanlist = $joinedvals if ($authvfield->{fieldname} eq "items.notforloan");
}
}
$statussth =~ s, and $,,g;
$template->param(
branchloop => \@branch_loop,
authorised_values => \@authorised_value_list,
today => dt_from_string,
minlocation => $minlocation,
maxlocation => $maxlocation,
location => $location,
ignoreissued => $ignoreissued,
branchcode => $branchcode,
branch => $branch,
datelastseen => $datelastseen,
compareinv2barcd => $compareinv2barcd,
notforloanlist => $notforloanlist
);
my @notforloans;
if (defined $notforloanlist) {
@notforloans = split(/,/, $notforloanlist);
}
my @scanned_items;
my @errorloop;
if ( $uploadbarcodes && length($uploadbarcodes) > 0 ) {
my $dbh = C4::Context->dbh;
my $date = dt_from_string( $input->param('setdate') );
$date = output_pref ( { dt => $date, dateformat => 'iso' } );
my $strsth = "select * from issues, items where items.itemnumber=issues.itemnumber and items.barcode =?";
my $qonloan = $dbh->prepare($strsth);
$strsth="select * from items where items.barcode =? and items.withdrawn = 1";
my $qwithdrawn = $dbh->prepare($strsth);
my $count = 0;
my @barcodes;
my @uploadedbarcodes;
my $sth = $dbh->column_info(undef,undef,"items","barcode");
my $barcode_def = $sth->fetchall_hashref('COLUMN_NAME');
my $barcode_size = $barcode_def->{barcode}->{COLUMN_SIZE};
my $err_length=0;
my $err_data=0;
my $lines_read=0;
binmode($uploadbarcodes, ":encoding(UTF-8)");
while (my $barcode=<$uploadbarcodes>) {
$barcode =~ s/\r/\n/g;
$barcode =~ s/\n\n/\n/g;
my @data = split(/\n/,$barcode);
push @uploadedbarcodes, @data;
}
for my $barcode (@uploadedbarcodes) {
next unless $barcode;
++$lines_read;
if (length($barcode)>$barcode_size) {
$err_length += 1;
}
my $check_barcode = $barcode;
$check_barcode =~ s/\p{Print}//g;
if (length($check_barcode)>0) { # Only printable unicode characters allowed.
$err_data += 1;
}
next if length($barcode)>$barcode_size;
next if ( length($check_barcode)>0 );
push @barcodes,$barcode;
}
$template->param( LinesRead => $lines_read );
if (! @barcodes) {
push @errorloop, {'barcode'=>'No valid barcodes!'};
$op=''; # force the initial inventory screen again.
}
else {
$template->param( err_length => $err_length,
err_data => $err_data );
}
foreach my $barcode (@barcodes) {
if ( $qwithdrawn->execute($barcode) && $qwithdrawn->rows ) {
push @errorloop, { 'barcode' => $barcode, 'ERR_WTHDRAWN' => 1 };
} else {
my $item = GetItem( '', $barcode );
if ( defined $item && $item->{'itemnumber'} ) {
ModItem( { datelastseen => $date }, undef, $item->{'itemnumber'} );
push @scanned_items, $item;
$count++;
$qonloan->execute($barcode);
if ($qonloan->rows){
my $data = $qonloan->fetchrow_hashref;
my ($doreturn, $messages, $iteminformation, $borrower) =AddReturn($barcode, $data->{homebranch});
if ($doreturn){
push @errorloop, {'barcode'=>$barcode,'ERR_ONLOAN_RET'=>1}
} else {
push @errorloop, {'barcode'=>$barcode,'ERR_ONLOAN_NOT_RET'=>1}
}
}
} else {
push @errorloop, {'barcode'=>$barcode,'ERR_BARCODE'=>1};
}
}
}
$template->param( date => $date, Number => $count );
$template->param( errorloop => \@errorloop ) if (@errorloop);
}
# now build the result list: inventoried items if requested, and mis-placed items -always-
my $inventorylist;
my $wrongplacelist;
my @items_with_problems;
if ( $markseen or $op ) {
# retrieve all items in this range.
my $totalrecords;
# We use datelastseen only when comparing the results to the barcode file.
my $paramdatelastseen = ($compareinv2barcd) ? $datelastseen : '';
($inventorylist, $totalrecords) = GetItemsForInventory( {
minlocation => $minlocation,
maxlocation => $maxlocation,
location => $location,
itemtype => $itemtype,
ignoreissued => $ignoreissued,
datelastseen => $paramdatelastseen,
branchcode => $branchcode,
branch => $branch,
offset => 0,
size => undef,
statushash => $staton,
interface => 'staff',
} );
# For the items that may be marked as "wrong place", we only check the location (callnumbers, location and branch)
($wrongplacelist, $totalrecords) = GetItemsForInventory( {
minlocation => $minlocation,
maxlocation => $maxlocation,
location => $location,
itemtype => undef,
ignoreissued => undef,
datelastseen => undef,
branchcode => $branchcode,
branch => $branch,
offset => 0,
size => undef,
statushash => undef,
interface => 'staff',
} );
}
# If "compare barcodes list to results" has been checked, we want to alert for missing items
if ( $compareinv2barcd ) {
# set "missing" flags for all items with a datelastseen (dls) before the choosen datelastseen (cdls)
my $dls = output_pref( { dt => dt_from_string( $datelastseen ),
dateformat => 'iso' } );
foreach my $item ( @$inventorylist ) {
my $cdls = output_pref( { dt => dt_from_string( $_->{datelastseen} ),
dateformat => 'iso' } );
if ( $cdls lt $dls ) {
$item->{problem} = 'missingitem';
# We have to push a copy of the item, not the reference
push @items_with_problems, { %$item };
}
}
}
# insert "wrongplace" to all scanned items that are not supposed to be in this range
# note this list is always displayed, whatever the librarian has choosen for comparison
my $moddatecount = 0;
foreach my $item ( @scanned_items ) {
# Saving notforloan code before it's replaced by it's authorised value for later comparison
$item->{notforloancode} = $item->{notforloan};
# Populating with authorised values
foreach my $field ( keys %$item ) {
# If the koha field is mapped to a marc field
my $fc = $item->{'frameworkcode'} || '';
my ($f, $sf) = GetMarcFromKohaField("items.$field", $fc);
if ($f and $sf) {
# We replace the code with it's description
my $authvals = C4::Koha::GetKohaAuthorisedValuesFromField($f, $sf, $fc);
if ($authvals and defined $item->{$field} and defined $authvals->{$item->{$field}}) {
$item->{$field} = $authvals->{$item->{$field}};
}
}
}
next if $item->{onloan}; # skip checked out items
# If we have scanned items with a non-matching notforloan value
if (none { $item->{'notforloancode'} eq $_ } @notforloans) {
$item->{problem} = 'changestatus';
push @items_with_problems, { %$item };
}
if (none { $item->{barcode} eq $_->{barcode} && !$_->{'onloan'} } @$wrongplacelist) {
$item->{problem} = 'wrongplace';
push @items_with_problems, { %$item };
}
# Modify date last seen for scanned items
ModDateLastSeen($_->{'itemnumber'});
$moddatecount++;
}
if ( $compareinv2barcd ) {
my @scanned_barcodes = map {$_->{barcode}} @scanned_items;
for my $should_be_scanned ( @$inventorylist ) {
my $barcode = $should_be_scanned->{barcode};
unless ( grep /^$barcode$/, @scanned_barcodes ) {
$should_be_scanned->{problem} = 'not_scanned';
push @items_with_problems, { %$should_be_scanned };
}
}
}
for my $item ( @items_with_problems ) {
my $biblio = C4::Biblio::GetBiblioData($item->{biblionumber});
$item->{title} = $biblio->{title};
$item->{author} = $biblio->{author};
}
# If a barcode file is given, we want to show problems, else all items
my @results;
@results = $uploadbarcodes
? @items_with_problems
: $op
? @$inventorylist
: ();
$template->param(
moddatecount => $moddatecount,
loop => \@results,
op => $op
);
if (defined $input->param('CSVexport') && $input->param('CSVexport') eq 'on'){
eval {use Text::CSV};
my $csv = Text::CSV->new or
die Text::CSV->error_diag ();
binmode STDOUT, ":encoding(UTF-8)";
print $input->header(
-type => 'text/csv',
-attachment => 'inventory.csv',
);
my $columns_def_hashref = C4::Reports::Guided::_get_column_defs($input);
foreach my $key ( keys %$columns_def_hashref ) {
my $initkey = $key;
$key =~ s/[^\.]*\.//;
$columns_def_hashref->{$initkey}=NormalizeString($columns_def_hashref->{$initkey} // '');
$columns_def_hashref->{$key} = $columns_def_hashref->{$initkey};
}
my @translated_keys;
for my $key (qw / biblioitems.title biblio.author
items.barcode items.itemnumber
items.homebranch items.location
items.itemcallnumber items.notforloan
items.itemlost items.damaged
items.withdrawn items.stocknumber
/ ) {
push @translated_keys, $columns_def_hashref->{$key};
}
push @translated_keys, 'problem' if $uploadbarcodes;
$csv->combine(@translated_keys);
print $csv->string, "\n";
my @keys = qw / title author barcode itemnumber homebranch location itemcallnumber notforloan lost damaged withdrawn stocknumber /;
for my $item ( @results ) {
my @line;
for my $key (@keys) {
push @line, $item->{$key};
}
if ( defined $item->{problem} ) {
if ( $item->{problem} eq 'wrongplace' ) {
push @line, "wrong place";
} elsif ( $item->{problem} eq 'missingitem' ) {
push @line, "missing item";
} elsif ( $item->{problem} eq 'changestatus' ) {
push @line, "change item status";
} elsif ($item->{problem} eq 'not_scanned' ) {
push @line, "item not scanned";
}
}
$csv->combine(@line);
print $csv->string, "\n";
}
# Adding not found barcodes
foreach my $error (@errorloop) {
my @line;
if ($error->{'ERR_BARCODE'}) {
push @line, map { $_ eq 'barcode' ? $error->{'barcode'} : ''} @keys;
push @line, "barcode not found";
$csv->combine(@line);
print $csv->string, "\n";
}
}
exit;
}
output_html_with_http_headers $input, $cookie, $template->output;