@@ -, +, @@ requested, the comparison was wrong: the logic was wrong didn't had the author and title, it was hard to retrieve them on the shelved informations as the screen * scan a list of barcode & select a range of location * if a barcode has been scanned and should not be (mis placed item), the information is displayed * if you choose "compare barcodes list to result option", the resulting list contains all items that have been scanned and those that were supposed to be. Any item not in both list appears with a specific message on the last column --- C4/Auth.pm | 2 +- C4/Items.pm | 41 ++- C4/Templates.pm | 2 +- koha-tmpl/intranet-tmpl/prog/en/columns.def | 1 + .../prog/en/modules/tools/inventory.tt | 188 ++++++++------ tools/ajax-inventory.pl | 43 ++++ tools/inventory.pl | 272 ++++++++++++++------ 7 files changed, 377 insertions(+), 172 deletions(-) create mode 100755 tools/ajax-inventory.pl --- a/C4/Auth.pm +++ a/C4/Auth.pm @@ -1144,7 +1144,7 @@ sub check_api_auth { unless ($query->param('userid')) { $sessionID = $query->cookie("CGISESSID"); } - if ($sessionID && not ($cas && $query->param('PT')) ) { + if ( $sessionID && not ($cas && $query->param('PT')) ) { my $session = get_session($sessionID); C4::Context->_new_userenv($sessionID); if ($session) { --- a/C4/Items.pm +++ a/C4/Items.pm @@ -970,9 +970,11 @@ sub GetLostItems { =head2 GetItemsForInventory - $itemlist = GetItemsForInventory($minlocation, $maxlocation, - $location, $itemtype $datelastseen, $branch, - $offset, $size, $statushash); +=over 4 + +($itemlist, $iTotalRecords) = GetItemsForInventory($minlocation, $maxlocation, $location, $itemtype, $ignoreissued, $datelastseen, $branchcode, $offset, $size, $statushash); + +=back Retrieve a list of title/authors/barcode/callnumber, for biblio inventory. @@ -985,6 +987,8 @@ the datelastseen can be used to specify that you want to see items not seen sinc offset & size can be used to retrieve only a part of the whole listing (defaut behaviour) $statushash requires a hashref that has the authorized values fieldname (intems.notforloan, etc...) as keys, and an arrayref of statuscodes we are searching for as values. +$iTotalRecords is the number of rows that would have been returned without the $offset, $size limit clause + =cut sub GetItemsForInventory { @@ -993,7 +997,7 @@ sub GetItemsForInventory { my ( @bind_params, @where_strings ); my $query = <<'END_SQL'; -SELECT items.itemnumber, barcode, itemcallnumber, title, author, biblio.biblionumber, datelastseen +SELECT SQL_CALC_FOUND_ROWS items.itemnumber, barcode, itemcallnumber, title, author, biblio.biblionumber, biblio.frameworkcode, datelastseen, homebranch, location, notforloan, damaged, itemlost, stocknumber FROM items LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber LEFT JOIN biblioitems on items.biblionumber = biblioitems.biblionumber @@ -1052,20 +1056,33 @@ END_SQL $query .= join ' AND ', @where_strings; } $query .= ' ORDER BY items.cn_sort, itemcallnumber, title'; + $query .= " LIMIT $offset, $size" if ($offset and $size); my $sth = $dbh->prepare($query); $sth->execute( @bind_params ); my @results; - $size--; - while ( my $row = $sth->fetchrow_hashref ) { - $offset-- if ($offset); - $row->{datelastseen}=format_date($row->{datelastseen}); - if ( ( !$offset ) && $size ) { - push @results, $row; - $size--; + my $tmpresults = $sth->fetchall_arrayref({}); + $sth = $dbh->prepare("SELECT FOUND_ROWS()"); + $sth->execute(); + my ($iTotalRecords) = $sth->fetchrow_array(); + + foreach my $row (@$tmpresults) { + $row->{datelastseen} = format_date( $row->{datelastseen} ); + + # Auth values + foreach (keys %$row) { + # If the koha field is mapped to a marc field + my ($f, $sf) = GetMarcFromKohaField("items.$_", $row->{'frameworkcode'}); + if ($f and $sf) { + # We replace the code with it's description + my $authvals = C4::Koha::GetKohaAuthorisedValuesFromField($f, $sf, $row->{'frameworkcode'}); + $row->{$_} = $authvals->{$row->{$_}} if defined $authvals->{$row->{$_}}; + } } + push @results, $row; } - return \@results; + + return (\@results, $iTotalRecords); } =head2 GetItemsCount --- a/C4/Templates.pm +++ a/C4/Templates.pm @@ -288,7 +288,7 @@ sub getlanguage { my $lang; # cookie - if ( $query->cookie('KohaOpacLanguage') ) { + if ($query and $query->cookie('KohaOpacLanguage') ) { $lang = $query->cookie('KohaOpacLanguage'); $lang =~ s/[^a-zA-Z_-]*//; # sanitize cookie } --- a/koha-tmpl/intranet-tmpl/prog/en/columns.def +++ a/koha-tmpl/intranet-tmpl/prog/en/columns.def @@ -138,3 +138,4 @@ biblioitems.place Place of publication biblioitems.lccn LCCN biblioitems.marcxml MARC blob biblioitems.url URL +biblioitems.title Title --- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/inventory.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/inventory.tt @@ -1,11 +1,53 @@ [% INCLUDE 'doc-head-open.inc' %] Koha › Tools › Inventory [% INCLUDE 'doc-head-close.inc' %] + + + [% INCLUDE 'calendar.inc' %] - + [% INCLUDE 'header.inc' %] [% INCLUDE 'cat-search.inc' %] - +
@@ -40,26 +82,25 @@ $(document).ready(function(){

Inventory/Stocktaking

- [% IF ( Number ) %]
[% Number %] items modified : datelastseen set to [% date %]
[% END %] - [% IF ( errorfile ) %]
[% errorfile %] can't be opened
[% END %] - [% FOREACH errorloo IN errorloop %] + [% IF (moddatecount) %]
[% moddatecount %] items modified : datelastseen set to [% date %]
[% END %] + [% IF (errorfile) %]
[% errorfile %] can't be opened
[% END %] + [% FOREACH error IN errorloop %]
- [% errorloo.barcode %] - [% IF ( errorloo.ERR_BARCODE ) %]: barcode not found[% END %] - [% IF ( errorloo.ERR_WTHDRAWN ) %]: item withdrawn[% END %] - [% IF ( errorloo.ERR_ONLOAN_RET ) %]: item was on loan. It was returned before marked as seen[% END %] - [% IF ( errorloo.ERR_ONLOAN_NOT_RET ) %]: item was on loan. couldn't be returned.[% END %] + [% error.barcode %] + [% IF (error.ERR_BARCODE) %]: barcode not found[% END %] + [% IF (error.ERR_WTHDRAWN) %]: item withdrawn[% END %] + [% IF (error.ERR_ONLOAN_RET) %]: item was on loan. It was returned before marked as seen[% END %] + [% IF (error.ERR_ONLOAN_NOT_RET) %]: item was on loan. couldn't be returned.[% END %]
[% END %] - [% UNLESS ( loop ) %] + [% UNLESS op %]
Use a barcode file
  1. -
  2. -
    [% INCLUDE 'date-format.inc' %]
    +
@@ -81,43 +122,43 @@ $(document).ready(function(){ [% END %] - [% IF ( authorised_values ) %] + [% IF (authorised_values) %]
  • [% END %]
  • - + (items.itemcallnumber)
  • - [% IF ( statuses ) %] + [% IF (statuses) %]
    Item statuses
    - [% FOREACH statuse IN statuses %] - [% IF ( statuse.values ) %] + [% FOREACH status IN statuses %] + [% IF (status.values) %]
    - [% statuse.fieldname %] -
      - [% FOREACH value IN statuse.values %] - [% IF ( value.lib ) %]
    • + [% status.fieldname %] +
        + [% FOREACH value IN status.values %] + [% IF (value.lib) %]
      • - +
      • [% END %] [% END %]
      @@ -131,20 +172,14 @@ $(document).ready(function(){ [% END %]
    • - -
      [% INCLUDE 'date-format.inc' %]
      +
    • - [% IF ( ignoreissued ) %] + [% IF (ignoreissued) %]
    • [% ELSE %] [% END %] -
    • - items
    • -
    • - -
    • @@ -161,7 +196,7 @@ $(document).ready(function(){
    [% END %] - [% IF ( loop ) %] + [% IF (op) %] @@ -171,74 +206,65 @@ $(document).ready(function(){ -
    [Select all] [Clear all]
    - +
    [Select All] [Clear All]
    +
    + + + + + - [% FOREACH loo IN loop %] + + + [% FOREACH result IN loop %] + + + + [% END %] +
    Seen BarcodeLocation TitleStatusLostDamaged Unseen since Problems
    - + + + [% result.barcode | html %] - [% loo.barcode %] + [% result.homebranch | html %] [% result.location | html %] [[% result.itemcallnumber | html %]] -

    [% loo.itemcallnumber %] - [% loo.title |html %]

    -

    [% loo.author %]

    +

    [% result.title | html %]

    [% result.author | html %]

    -

    [% loo.datelastseen %]

    + [% result.notforloan | html %]
    - [% IF ( loo.notfoundbarcode ) %] -

    Not found among barcodes in barcodes file.

    - [% ELSIF ( loo.notfoundkoha ) %] -

    Not found in Koha.

    - [% ELSE %] -

    None

    - [% END %] + [% result.itemlost | html %] +
    + [% result.damaged | html %] + + [% result.datelastseen | html %] + + [% IF (result.wrongplace) %]

    Item should not have been scanned

    [% ELSIF (result.missingitem) %]

    Item missing

    [% ELSIF (result.changestatus) %]

    Change item status

    [% END %]
    -
    [Select all] [Clear all]
    - - - [% IF ( offset ) %] -
    - - - - - - - - - - -
    - [% END %] - [% IF ( nextoffset ) %] -
    - - - - - - - - - - +
    +

    +
    [Select All] [Clear All]
    + + +
    +
    - [% END %] + [% END %]
    --- a/tools/ajax-inventory.pl +++ a/tools/ajax-inventory.pl @@ -0,0 +1,43 @@ +#!/usr/bin/perl +# + +use Modern::Perl; + +use CGI; +use JSON; + +use C4::Auth; +use C4::Circulation qw/CanBookBeRenewed/; +use C4::Context; +use C4::Koha qw/getitemtypeimagelocation/; +use C4::Reserves qw/CheckReserves/; +use C4::Utils::DataTables; +use C4::Output; +use C4::Biblio; +use C4::Items; +use C4::Dates qw/format_date format_date_in_iso/; +use C4::Koha; +use C4::Branch; # GetBranches +use C4::Reports::Guided; #_get_column_defs +use C4::Charset; +use List::MoreUtils qw /none/; + + +my $input = new CGI; + +# Authentication +my ($status, $cookie, $sessionId) = C4::Auth::check_api_auth($input, { tools => 'inventory' }); +exit unless ($status eq "ok"); + + +my $seen = $input->param('seen'); +my @seent = split(/\|/, $seen); + +# mark seen if applicable (ie: coming form mark seen checkboxes) +foreach ( @seent ) { + /SEEN-(.+)/ and &ModDateLastSeen($1); +} + + +print $input->header('application/json'); + --- a/tools/inventory.pl +++ a/tools/inventory.pl @@ -35,6 +35,10 @@ use C4::Dates qw/format_date format_date_in_iso/; use C4::Koha; use C4::Branch; # GetBranches use C4::Circulation; +use C4::Reports::Guided; #_get_column_defs +use C4::Charset; +use List::MoreUtils qw/none/; + my $minlocation=$input->param('minlocation') || ''; my $maxlocation=$input->param('maxlocation'); @@ -50,19 +54,20 @@ my $pagesize = $input->param('pagesize'); $pagesize=50 unless $pagesize; my $branchcode = $input->param('branchcode') || ''; my $branch = $input->param('branch'); -my $op = $input->param('op'); -my $res; #contains the results loop -# warn "uploadbarcodes : ".$uploadbarcodes; -# use Data::Dumper; warn Dumper($input); - -my ($template, $borrowernumber, $cookie) - = get_template_and_user({template_name => "tools/inventory.tmpl", - query => $input, - type => "intranet", - authnotrequired => 0, - flagsrequired => {tools => 'inventory'}, - debug => 1, - }); +my $op = $input->param('op'); +my $compareinv2barcd = $input->param('compareinv2barcd'); +my $res; #contains the results loop + +my ( $template, $borrowernumber, $cookie ) = get_template_and_user( + { template_name => "tools/inventory.tmpl", + query => $input, + type => "intranet", + authnotrequired => 0, + flagsrequired => { tools => 'inventory' }, + debug => 1, + } +); + my $branches = GetBranches(); my @branch_loop; @@ -103,58 +108,76 @@ for my $statfield (qw/items.notforloan items.itemlost items.wthdrawn items.damag 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->{id}) && $input->param('status-' . $authvfield->{fieldname} . '-' . $authval->{id}) eq 'on' ){ - push @{$staton->{$authvfield->{fieldname}}}, $authval->{id}; + 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 => C4::Dates->today(), - minlocation => $minlocation, - maxlocation => $maxlocation, - location=>$location, - ignoreissued=>$ignoreissued, - branchcode=>$branchcode, - branch => $branch, - offset => $offset, - pagesize => $pagesize, - datelastseen => $datelastseen, - ); +$template->param( + branchloop => \@branch_loop, + authorised_values => \@authorised_value_list, + DHTMLcalendar_dateformat => C4::Dates->DHTMLcalendar(), + today => C4::Dates->today(), + minlocation => $minlocation, + maxlocation => $maxlocation, + location => $location, + ignoreissued => $ignoreissued, + branchcode => $branchcode, + branch => $branch, + offset => $offset, + pagesize => $pagesize, + datelastseen => $datelastseen, + compareinv2barcd => $compareinv2barcd, + notforloanlist => $notforloanlist +); + +my @notforloans; +if (defined $notforloanlist) { + @notforloans = split(/,/, $notforloanlist); +} + + + my @brcditems; -if ($uploadbarcodes && length($uploadbarcodes)>0){ - my $dbh=C4::Context->dbh; - my $date = format_date_in_iso($input->param('setdate')) || C4::Dates->today('iso'); -# warn "$date"; - my $strsth="select * from issues, items where items.itemnumber=issues.itemnumber and items.barcode =?"; +my $barcodelist; +my @errorloop; +if ( $uploadbarcodes && length($uploadbarcodes) > 0 ) { + my $dbh = C4::Context->dbh; + my $date = format_date_in_iso( $input->param('setdate') ) || C4::Dates->today('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.wthdrawn = 1"; my $qwthdrawn = $dbh->prepare($strsth); - my @errorloop; - my $count=0; - while (my $barcode=<$uploadbarcodes>){ + my $count = 0; + + while ( my $barcode = <$uploadbarcodes> ) { $barcode =~ s/\r?\n$//; - if ($qwthdrawn->execute($barcode) &&$qwthdrawn->rows){ - push @errorloop, {'barcode'=>$barcode,'ERR_WTHDRAWN'=>1}; - }else{ - my $item = GetItem('', $barcode); - if (defined $item && $item->{'itemnumber'}){ - ModItem({ datelastseen => $date }, undef, $item->{'itemnumber'}); + $barcodelist .= ($barcodelist) ? '|' . $barcode : $barcode; + if ( $qwthdrawn->execute($barcode) && $qwthdrawn->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 @brcditems, $item; $count++; $qonloan->execute($barcode); @@ -171,48 +194,102 @@ if ($uploadbarcodes && length($uploadbarcodes)>0){ push @errorloop, {'barcode'=>$barcode,'ERR_BARCODE'=>1}; } } + } $qonloan->finish; $qwthdrawn->finish; - $template->param(date=>format_date($date),Number=>$count); -# $template->param(errorfile=>$errorfile) if ($errorfile); - $template->param(errorloop=>\@errorloop) if (@errorloop); + $template->param( date => format_date($date), Number => $count ); + $template->param( errorloop => \@errorloop ) if (@errorloop); + + } -#if we want to compare the results to a list of barcodes, or we have no barcode file -if ( ! ($uploadbarcodes && length($uploadbarcodes)>0 ) || ( $input->param('compareinv2barcd') eq 'on' && length($uploadbarcodes)>0) ) { - if ($markseen) { - foreach ($input->param) { - /SEEN-(.+)/ and &ModDateLastSeen($1); - } - } - if ($markseen or $op) { - $res = GetItemsForInventory( $minlocation, $maxlocation, $location, $itemtype, $ignoreissued, $datelastseen, $branchcode, $branch, $offset, $pagesize, $staton ); - $template->param(loop =>$res, - nextoffset => ($offset+$pagesize), - prevoffset => ($offset?$offset-$pagesize:0), - ); +$template->param(barcodelist => $barcodelist); + +# now build the result list: inventoried items if requested, and mis-placed items -always- +my $inventorylist; +if ( $markseen or $op ) { + # retrieve all items in this range. + my $totalrecords; + ($inventorylist, $totalrecords) = GetItemsForInventory($minlocation, $maxlocation, $location, $itemtype, $ignoreissued, '', $branchcode, $branch, 0, undef , $staton); + + # Real copy + my @res_copy; + foreach (@$inventorylist) { + push @res_copy, $_; } - if ( defined $input->param('compareinv2barcd') && ( ( $input->param('compareinv2barcd') eq 'on' ) && ( scalar @brcditems != scalar @$res ) ) && length($uploadbarcodes) > 0 ){ - if ( scalar @brcditems > scalar @$res ){ - for my $brcditem (@brcditems) { - if (! grep( $_->{barcode} =~ /$brcditem->{barcode}/ , @$res) ){ - $brcditem->{notfoundkoha} = 1; - push @$res, $brcditem; - } - } - } else { - my @notfound; - for my $item (@$res) { - if ( ! grep( $_->{barcode} =~ /$item->{barcode}/ , @brcditems) ){ - $item->{notfoundbarcode} = 1; - push @notfound, $item; - } + $res = \@res_copy; +} + +# set "missing" flags for all items with a datelastseen before the choosen datelastseen +foreach (@$res) { $_->{missingitem}=1 if C4::Dates->new($_->{datelastseen})->output('iso') lt C4::Dates->new($datelastseen)->output('iso'); } + +# removing missing items from loop if "Compare barcodes list to results" has not been checked +@$res = grep {!$_->{missingitem} == 1 } @$res if (!$input->param('compareinv2barcd')); + +# 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 +foreach my $temp (@brcditems) { + + # Saving notforloan code before it's replaced by it's authorised value for later comparison + $temp->{'notforloancode'} = $temp->{'notforloan'}; + + # Populating with authorised values + foreach (keys %$temp) { + # If the koha field is mapped to a marc field + my $fc = $temp->{'frameworkcode'} || ''; + my ($f, $sf) = GetMarcFromKohaField("items.$_", $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 $temp->{$_} and defined $authvals->{$temp->{$_}}) { + $temp->{$_} = $authvals->{$temp->{$_}}; } - $res = [@$res, @notfound]; } } + + next if $temp->{onloan}; # skip checked out items + + # If we have scanned items with a non-matching notforloan value + if (none { $temp->{'notforloancode'} eq $_ } @notforloans) { + $temp->{'changestatus'} = 1; + my $biblio = C4::Biblio::GetBiblioData($temp->{biblionumber}); + $temp->{title} = $biblio->{title}; + $temp->{author} = $biblio->{author}; + $temp->{datelastseen} = format_date($temp->{datelastseen}); + push @$res, $temp; + + } + if (none { $temp->{barcode} eq $_->{barcode} && !$_->{'onloan'} } @$inventorylist) { + my $temp2 = { %$temp }; + $temp2->{wrongplace}=1; + my $biblio = C4::Biblio::GetBiblioData($temp->{biblionumber}); + $temp2->{title} = $biblio->{title}; + $temp2->{author} = $biblio->{author}; + $temp2->{datelastseen} = format_date($temp->{datelastseen}); + push @$res, $temp2; + } +} + +# Finally, modifying datelastseen for remaining items +my $moddatecount = 0; +foreach (@$res) { + unless ($_->{'missingitem'}) { + ModDateLastSeen($_->{'itemnumber'}); + $moddatecount++; + } } +# Removing items that don't have any problems from loop +@$res = grep { $_->{missingitem} || $_->{wrongplace} || $_->{changestatus} } @$res; + +$template->param( + moddatecount => $moddatecount, + loop => $res, + nextoffset => ( $offset + $pagesize ), + prevoffset => ( $offset ? $offset - $pagesize : 0 ), + op => $op +); + if (defined $input->param('CSVexport') && $input->param('CSVexport') eq 'on'){ eval {use Text::CSV}; my $csv = Text::CSV->new or @@ -221,14 +298,55 @@ if (defined $input->param('CSVexport') && $input->param('CSVexport') eq 'on'){ -type => 'text/csv', -attachment => 'inventory.csv', ); - for my $re (@$res){ + + my $columns_def_hashref = C4::Reports::Guided::_get_column_defs(); + 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.stocknumber + / ) { + push @translated_keys, $columns_def_hashref->{$key}; + } + + $csv->combine(@translated_keys); + print $csv->string, "\n"; + + my @keys = qw / title author barcode itemnumber homebranch location itemcallnumber notforloan lost damaged stocknumber /; + for my $re (@$res) { my @line; - for my $key (keys %$re) { + for my $key (@keys) { push @line, $re->{$key}; } + if ($re->{wrongplace}) { + push @line, "wrong place"; + } elsif ($re->{missingitem}) { + push @line, "missing item"; + } elsif ($re->{changestatus}) { + push @line, "change item status"; + } $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; } --