Bug 7295: (follow-up) several fixes
[koha.git] / offline_circ / process_koc.pl
index 2df542e..75ff319 100755 (executable)
 # 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, write to the Free Software Foundation, Inc., 59 Temple Place,
-# Suite 330, Boston, MA  02111-1307 USA
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 #
 
 use strict;
-require Exporter;
+use warnings;
 
 use CGI;
 use C4::Output;
@@ -29,142 +29,363 @@ use C4::Context;
 use C4::Biblio;
 use C4::Accounts;
 use C4::Circulation;
+use C4::Items;
 use C4::Members;
 use C4::Stats;
+use C4::UploadedFile;
+use C4::BackgroundJob;
 
 use Date::Calc qw( Add_Delta_Days Date_to_Days );
 
 use constant DEBUG => 0;
 
-our $query = new CGI;
+# this is the file version number that we're coded against.
+my $FILE_VERSION = '1.0';
 
-my ($template, $loggedinuser, $cookie)
-  = get_template_and_user( { template_name => "offline_circ/process_koc.tmpl",
-                               query => $query,
-                               type => "intranet",
-                               authnotrequired => 1,
-                               debug => 1,
-                               });
+our $query = CGI->new;
 
+my ($template, $loggedinuser, $cookie) = get_template_and_user({
+    template_name => "offline_circ/process_koc.tmpl",
+    query => $query,
+    type => "intranet",
+    authnotrequired => 0,
+     flagsrequired   => { circulate => "circulate_remaining_permissions" },
+});
+
+
+my $fileID=$query->param('uploadedfileid');
+my $runinbackground = $query->param('runinbackground');
+my $completedJobID = $query->param('completedJobID');
+my %cookies = parse CGI::Cookie($cookie);
+my $sessionID = $cookies{'CGISESSID'}->value;
 ## 'Local' globals.
 our $dbh = C4::Context->dbh();
+our @output = (); ## For storing messages to be displayed to the user
 
-our $branchcode = C4::Context->userenv->{branch};
 
-warn "Branchcode: $branchcode";
+if ($completedJobID) {
+    my $job = C4::BackgroundJob->fetch($sessionID, $completedJobID);
+    my $results = $job->results();
+    $template->param(transactions_loaded => 1);
+    $template->param(messages => $results->{results});
+} elsif ($fileID) {
+    my $uploaded_file = C4::UploadedFile->fetch($sessionID, $fileID);
+    my $fh = $uploaded_file->fh();
+    my @input_lines = <$fh>;
 
-our @output; ## For storing messages to be displayed to the user
+    my $filename = $uploaded_file->name();
+    my $job = undef;
 
-$query::POST_MAX = 1024 * 10000;
+    if ($runinbackground) {
+        my $job_size = scalar(@input_lines);
+        $job = C4::BackgroundJob->new($sessionID, $filename, $ENV{'SCRIPT_NAME'}, $job_size);
+        my $jobID = $job->id();
 
-my $file = $query->param("kocfile");
-$file=~m/^.*(\\|\/)(.*)/; # strip the remote path and keep the filename 
-my $name = $file; 
+        # fork off
+        if (my $pid = fork) {
+            # parent
+            # return job ID as JSON
 
-my $header = <$file>;
+            # prevent parent exiting from
+            # destroying the kid's database handle
+            # FIXME: according to DBI doc, this may not work for Oracle
+            $dbh->{InactiveDestroy}  = 1;
 
-while ( my $line = <$file> ) {
-  my ( $type, $cardnumber, $barcode, $datetime ) = split( /\t/, $line );
-  ( $datetime ) = split( /\+/, $datetime );
-  my ( $date ) = split( / /, $datetime );
+            my $reply = CGI->new("");
+            print $reply->header(-type => 'text/html');
+            print '{"jobID":"' . $jobID . '"}';
+            exit 0;
+        } elsif (defined $pid) {
+            # child
+            # close STDOUT to signal to Apache that
+            # we're now running in the background
+            close STDOUT;
+            close STDERR;
+        } else {
+            # fork failed, so exit immediately
+            # fork failed, so exit immediately
+            warn "fork failed while attempting to run $ENV{'SCRIPT_NAME'} as a background job";
+            exit 0;
+        }
 
-  my $circ;
-  $circ->{ 'type' } = $type;
-  $circ->{ 'cardnumber' } = $cardnumber;
-  $circ->{ 'barcode' } = $barcode;
-  $circ->{ 'datetime' } = $datetime;
-  $circ->{ 'date' } = $date;
-  
-  if ( $circ->{ 'type' } eq 'issue' ) {
-    kocIssueItem( $circ, $branchcode );
-  } elsif ( $circ->{ 'type' } eq 'return' ) {
-    kocReturnItem( $circ );
-  } elsif ( $circ->{ 'type' } eq 'payment' ) {
-      $circ->{'payment_amount'} = delete $circ->{'barcode'};
-    kocMakePayment( $circ );
-  }
-}
+        # if we get here, we're a child that has detached
+        # itself from Apache
+
+    }
+
+    my $header_line = shift @input_lines;
+    my $file_info   = parse_header_line($header_line);
+    if ($file_info->{'Version'} ne $FILE_VERSION) {
+        push @output, {
+            message => 1,
+            ERROR_file_version => 1,
+            upload_version => $file_info->{'Version'},
+            current_version => $FILE_VERSION
+        };
+    }
+
+    my $i = 0;
+    foreach  my $line (@input_lines)  {
+        $i++;
+        my $command_line = parse_command_line($line);
 
-$template->param(
-               intranetcolorstylesheet => C4::Context->preference("intranetcolorstylesheet"),
-               intranetstylesheet => C4::Context->preference("intranetstylesheet"),
-               IntranetNav => C4::Context->preference("IntranetNav"),
+        # map command names in the file to subroutine names
+        my %dispatch_table = (
+            issue     => \&kocIssueItem,
+            'return'  => \&kocReturnItem,
+            payment   => \&kocMakePayment,
+        );
+
+        # call the right sub name, passing the hashref of command_line to it.
+        if ( exists $dispatch_table{ $command_line->{'command'} } ) {
+            $dispatch_table{ $command_line->{'command'} }->($command_line);
+        } else {
+            warn "unknown command: '$command_line->{command}' not processed";
+        }
+
+        if ($runinbackground) {
+            $job->progress($i);
+        }
+    }
+
+    if ($runinbackground) {
+        $job->finish({ results => \@output }) if defined($job);
+    } else {
+        $template->param(transactions_loaded => 1);
+        $template->param(messages => \@output);
+    }
+}
 
-                messages => \@output,
-       );
 output_html_with_http_headers $query, $cookie, $template->output;
 
+=head1 FUNCTIONS
+
+=head2 parse_header_line
+
+parses the header line from a .koc file. This is the line that
+specifies things such as the file version, and the name and version of
+the offline circulation tool that generated the file. See
+L<http://wiki.koha-community.org/wiki/Koha_offline_circulation_file_format>
+for more information.
+
+pass in a string containing the header line (the first line from th
+file).
+
+returns a hashref containing the information from the header.
+
+=cut
+
+sub parse_header_line {
+    my $header_line = shift;
+    chomp($header_line);
+    $header_line =~ s/\r//g;
+
+    my @fields = split( /\t/, $header_line );
+    my %header_info = map { split( /=/, $_ ) } @fields;
+    return \%header_info;
+}
+
+=head2 parse_command_line
+
+=cut
+
+sub parse_command_line {
+    my $command_line = shift;
+    chomp($command_line);
+    $command_line =~ s/\r//g;
+
+    my ( $timestamp, $command, @args ) = split( /\t/, $command_line );
+    my ( $date,      $time,    $id )   = split( /\s/, $timestamp );
+
+    my %command = (
+        date    => $date,
+        time    => $time,
+        id      => $id,
+        command => $command,
+    );
+
+    # set the rest of the keys using a hash slice
+    my $argument_names = arguments_for_command($command);
+    @command{@$argument_names} = @args;
+
+    return \%command;
+
+}
+
+=head2 arguments_for_command
+
+fetches the names of the columns (and function arguments) found in the
+.koc file for a particular command name. For instance, the C<issue>
+command requires a C<cardnumber> and C<barcode>. In that case this
+function returns a reference to the list C<qw( cardnumber barcode )>.
+
+parameters: the command name
+
+returns: listref of column names.
+
+=cut
+
+sub arguments_for_command {
+    my $command = shift;
+
+    # define the fields for this version of the file.
+    my %format = (
+        issue   => [qw( cardnumber barcode )],
+        return  => [qw( barcode )],
+        payment => [qw( cardnumber amount )],
+    );
+
+    return $format{$command};
+}
+
 sub kocIssueItem {
-  my ( $circ, $branchcode ) = @_;
-
-  my $borrower = GetMember( $circ->{ 'cardnumber' }, 'cardnumber' );
-  my $item = GetBiblioFromItemNumber( undef, $circ->{ 'barcode' } );
-  my $issue = GetItemIssue( $item->{'itemnumber'} );
-
-  my $issuingrule = GetIssuingRule( $borrower->{ 'categorycode' }, $item->{ 'itemtype' }, $branchcode );
-  my $issuelength = $issuingrule->{ 'issuelength' };
-  my ( $year, $month, $day ) = split( /-/, $circ->{'date'} );
-  ( $year, $month, $day ) = Add_Delta_Days( $year, $month, $day, $issuelength );
-  my $date_due = sprintf("%04d-%02d-%02d", $year, $month, $day);
-  
-  if ( $issue->{ 'date_due' } ) { ## Item is currently checked out to another person.
-warn "Item Currently Issued.";
-    my $issue = GetOpenIssue( $item->{'itemnumber'} );
-
-    if ( $issue->{'borrowernumber'} eq $borrower->{'borrowernumber'} ) { ## Issued to this person already, renew it.
-warn "Item issued to this member already, renewing.";
-    
-    my $date_due_object = C4::Dates->new($date_due ,'iso');
-    C4::Circulation::AddRenewal(
-        $issue->{'borrowernumber'},    # borrowernumber
-        $item->{'itemnumber'},         # itemnumber
-        undef,                         # branch
-        $date_due_object,              # datedue
-        $circ->{'date'},               # issuedate
-    ) unless ($DEBUG);
-
-      push( @output, { message => "Renewed $item->{ 'title' } ( $item->{ 'barcode' } ) to $borrower->{ 'firstname' } $borrower->{ 'surename' } ( $borrower->{'cardnumber'} ) : $circ->{ 'datetime' }\n" } );
+    my $circ = shift;
 
-    } else {
-warn "Item issued to a different member.";
-warn "Date of previous issue: $issue->{'issuedate'}";
-warn "Date of this issue: $circ->{'date'}";
-      my ( $i_y, $i_m, $i_d ) = split( /-/, $issue->{'issuedate'} );
-      my ( $c_y, $c_m, $c_d ) = split( /-/, $circ->{'date'} );
-      
-      if ( Date_to_Days( $i_y, $i_m, $i_d ) < Date_to_Days( $c_y, $c_m, $c_d ) ) { ## Current issue to a different persion is older than this issue, return and issue.
-        C4::Circulation::AddIssue( $borrower, $circ->{'barcode'}, $date_due ) unless ( DEBUG );
-        push( @output, { message => "Issued $item->{ 'title' } ( $item->{ 'barcode' } ) to $borrower->{ 'firstname' } $borrower->{ 'surename' } ( $borrower->{'cardnumber'} ) : $circ->{ 'datetime' }\n" } );
-
-      } else { ## Current issue is *newer* than this issue, write a 'returned' issue, as the item is most likely in the hands of someone else now.
-warn "Current issue to another member is newer. Doing nothing";
-        ## This situation should only happen of the Offline Circ data is *really* old.
-        ## FIXME: write line to old_issues and statistics
-      }
-    
+    $circ->{ 'barcode' } = barcodedecode($circ->{'barcode'}) if( $circ->{'barcode'} && C4::Context->preference('itemBarcodeInputFilter'));
+    my $branchcode = C4::Context->userenv->{branch};
+    my $borrower = GetMember( 'cardnumber'=>$circ->{ 'cardnumber' } );
+    my $item = GetBiblioFromItemNumber( undef, $circ->{ 'barcode' } );
+    my $issue = GetItemIssue( $item->{'itemnumber'} );
+
+    if ( $issue->{ 'date_due' } ) { ## Item is currently checked out to another person.
+        #warn "Item Currently Issued.";
+        my $issue = GetOpenIssue( $item->{'itemnumber'} );
+
+        if ( $issue->{'borrowernumber'} eq $borrower->{'borrowernumber'} ) { ## Issued to this person already, renew it.
+            #warn "Item issued to this member already, renewing.";
+
+            C4::Circulation::AddRenewal(
+                $issue->{'borrowernumber'},    # borrowernumber
+                $item->{'itemnumber'},         # itemnumber
+                undef,                         # branch
+                undef,                         # datedue - let AddRenewal calculate it automatically
+                $circ->{'date'},               # issuedate
+            ) unless ($DEBUG);
+
+            push @output, {
+                renew => 1,
+                title => $item->{ 'title' },
+                biblionumber => $item->{'biblionumber'},
+                barcode => $item->{ 'barcode' },
+                firstname => $borrower->{ 'firstname' },
+                surname => $borrower->{ 'surname' },
+                borrowernumber => $borrower->{'borrowernumber'},
+                cardnumber => $borrower->{'cardnumber'},
+                datetime => $circ->{ 'datetime' }
+            };
+
+        } else {
+            #warn "Item issued to a different member.";
+            #warn "Date of previous issue: $issue->{'issuedate'}";
+            #warn "Date of this issue: $circ->{'date'}";
+            my ( $i_y, $i_m, $i_d ) = split( /-/, $issue->{'issuedate'} );
+            my ( $c_y, $c_m, $c_d ) = split( /-/, $circ->{'date'} );
+
+            if ( Date_to_Days( $i_y, $i_m, $i_d ) < Date_to_Days( $c_y, $c_m, $c_d ) ) { ## Current issue to a different persion is older than this issue, return and issue.
+                C4::Circulation::AddIssue( $borrower, $circ->{'barcode'}, undef, undef, $circ->{'date'} ) unless ( DEBUG );
+                push @output, {
+                    issue => 1,
+                    title => $item->{ 'title' },
+                    biblionumber => $item->{'biblionumber'},
+                    barcode => $item->{ 'barcode' },
+                    firstname => $borrower->{ 'firstname' },
+                    surname => $borrower->{ 'surname' },
+                    borrowernumber => $borrower->{'borrowernumber'},
+                    cardnumber => $borrower->{'cardnumber'},
+                    datetime => $circ->{ 'datetime' }
+                };
+
+            } else { ## Current issue is *newer* than this issue, write a 'returned' issue, as the item is most likely in the hands of someone else now.
+                #warn "Current issue to another member is newer. Doing nothing";
+                ## This situation should only happen of the Offline Circ data is *really* old.
+                ## FIXME: write line to old_issues and statistics
+            }
+        }
+    } else { ## Item is not checked out to anyone at the moment, go ahead and issue it
+        C4::Circulation::AddIssue( $borrower, $circ->{'barcode'}, undef, undef, $circ->{'date'} ) unless ( DEBUG );
+        push @output, {
+            issue => 1,
+            title => $item->{ 'title' },
+            biblionumber => $item->{'biblionumber'},
+            barcode => $item->{ 'barcode' },
+            firstname => $borrower->{ 'firstname' },
+            surname => $borrower->{ 'surname' },
+            borrowernumber => $borrower->{'borrowernumber'},
+            cardnumber => $borrower->{'cardnumber'},
+            datetime =>$circ->{ 'datetime' }
+        };
     }
-  } else { ## Item is not checked out to anyone at the moment, go ahead and issue it
-      C4::Circulation::AddIssue( $borrower, $circ->{'barcode'}, $date_due ) unless ( DEBUG );
-    push( @output, { message => "Issued $item->{ 'title' } ( $item->{ 'barcode' } ) to $borrower->{ 'firstname' } $borrower->{ 'surename' } ( $borrower->{'cardnumber'} ) : $circ->{ 'datetime' }\n" } );
-  }  
 }
 
 sub kocReturnItem {
-  my ( $circ ) = @_;
-  my $borrower = GetMember( $circ->{ 'cardnumber' }, 'cardnumber' );
-  my $item = GetBiblioFromItemNumber( undef, $circ->{ 'barcode' } );
-  C4::Circulation::MarkIssueReturned( $borrower->{'borrowernumber'},
-                                      $item->{'itemnumber'},
-                                      undef,
-                                      $circ->{'date'} );
-  
-  push( @output, { message => "Returned $item->{ 'title' } ( $item->{ 'barcode' } ) From borrower number $borrower->{'borrowernumber'} : $circ->{ 'datetime' }\n" } ); 
+    my ( $circ ) = @_;
+    $circ->{'barcode'} = barcodedecode($circ->{'barcode'}) if( $circ->{'barcode'} && C4::Context->preference('itemBarcodeInputFilter'));
+    my $item = GetBiblioFromItemNumber( undef, $circ->{ 'barcode' } );
+    #warn( Data::Dumper->Dump( [ $circ, $item ], [ qw( circ item ) ] ) );
+    my $borrowernumber = _get_borrowernumber_from_barcode( $circ->{'barcode'} );
+    if ( $borrowernumber ) {
+        my $borrower = GetMember( 'borrowernumber' => $borrowernumber );
+        C4::Circulation::MarkIssueReturned(
+            $borrowernumber,
+            $item->{'itemnumber'},
+            undef,
+            $circ->{'date'},
+            $borrower->{'privacy'}
+        );
+
+        ModItem({ onloan => undef }, $item->{'biblionumber'}, $item->{'itemnumber'});
+        ModDateLastSeen( $item->{'itemnumber'} );
+
+        push @output, {
+            return => 1,
+            title => $item->{ 'title' },
+            biblionumber => $item->{'biblionumber'},
+            barcode => $item->{ 'barcode' },
+            borrowernumber => $borrower->{'borrowernumber'},
+            firstname => $borrower->{'firstname'},
+            surname => $borrower->{'surname'},
+            cardnumber => $borrower->{'cardnumber'},
+            datetime => $circ->{ 'datetime' }
+        };
+    } else {
+        push @output, {
+            ERROR_no_borrower_from_item => 1,
+            badbarcode => $circ->{'barcode'}
+        };
+    }
 }
 
 sub kocMakePayment {
-  my ( $circ ) = @_;
-  my $borrower = GetMember( $circ->{ 'cardnumber' }, 'cardnumber' );
-  recordpayment( $borrower->{'borrowernumber'}, $circ->{'payment_amount'} );
-  push( @output, { message => "accepted payment ($circ->{'payment_amount'}) from cardnumber ($circ->{'cardnumber'}), borrower ($borrower->{'borrowernumber'})" } );
+    my ( $circ ) = @_;
+    my $borrower = GetMember( 'cardnumber'=>$circ->{ 'cardnumber' } );
+    recordpayment( $borrower->{'borrowernumber'}, $circ->{'amount'} );
+    push @output, {
+        payment => 1,
+        amount => $circ->{'amount'},
+        firstname => $borrower->{'firstname'},
+        surname => $borrower->{'surname'},
+        cardnumber => $circ->{'cardnumber'},
+        borrower => $borrower->{'borrowernumber'}
+    };
 }
 
+=head2 _get_borrowernumber_from_barcode
+
+pass in a barcode
+get back the borrowernumber of the patron who has it checked out.
+undef if that can't be found
+
+=cut
+
+sub _get_borrowernumber_from_barcode {
+    my $barcode = shift;
+
+    return unless $barcode;
+
+    my $item = GetBiblioFromItemNumber( undef, $barcode );
+    return unless $item->{'itemnumber'};
+
+    my $issue = C4::Circulation::GetItemIssue( $item->{'itemnumber'} );
+    return unless $issue->{'borrowernumber'};
+    return $issue->{'borrowernumber'};
+}