# 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;
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
+
+
+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>;
+
+ my $filename = $uploaded_file->name();
+ my $job = undef;
+
+ if ($runinbackground) {
+ my $job_size = scalar(@input_lines);
+ $job = C4::BackgroundJob->new($sessionID, $filename, $ENV{'SCRIPT_NAME'}, $job_size);
+ my $jobID = $job->id();
+
+ # fork off
+ if (my $pid = fork) {
+ # parent
+ # return job ID as JSON
+
+ # prevent parent exiting from
+ # destroying the kid's database handle
+ # FIXME: according to DBI doc, this may not work for Oracle
+ $dbh->{InactiveDestroy} = 1;
+
+ 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;
+ }
+
+ # 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
+ };
+ }
-our $branchcode = C4::Context->userenv->{branch};
+ my $i = 0;
+ foreach my $line (@input_lines) {
+ $i++;
+ my $command_line = parse_command_line($line);
+
+ # 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);
+ }
+ }
-warn "Branchcode: $branchcode";
+ if ($runinbackground) {
+ $job->finish({ results => \@output }) if defined($job);
+ } else {
+ $template->param(transactions_loaded => 1);
+ $template->param(messages => \@output);
+ }
+}
-our @output; ## For storing messages to be displayed to the user
+output_html_with_http_headers $query, $cookie, $template->output;
-$query::POST_MAX = 1024 * 10000;
+=head1 FUNCTIONS
-my $file = $query->param("kocfile");
-$file=~m/^.*(\\|\/)(.*)/; # strip the remote path and keep the filename
-my $name = $file;
+=head2 parse_header_line
-my $header = <$file>;
+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.
-while ( my $line = <$file> ) {
- my ( $type, $cardnumber, $barcode, $datetime ) = split( /\t/, $line );
- ( $datetime ) = split( /\+/, $datetime );
- my ( $date ) = split( / /, $datetime );
+pass in a string containing the header line (the first line from th
+file).
- 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' ) {
- kocMakePayment( $circ );
- }
+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;
}
-$template->param(
- intranetcolorstylesheet => C4::Context->preference("intranetcolorstylesheet"),
- intranetstylesheet => C4::Context->preference("intranetstylesheet"),
- IntranetNav => C4::Context->preference("IntranetNav"),
+=head2 parse_command_line
- messages => \@output,
- );
-output_html_with_http_headers $query, $cookie, $template->output;
+=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 = "$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 $renewals = $issue->{'renewals'} + 1;
- ForceRenewal( $item->{'itemnumber'}, $circ->{'date'}, $date_due ) unless ( DEBUG );
-
- push( @output, { message => "Renewed $item->{ 'title' } ( $item->{ 'barcode' } ) to $borrower->{ 'firstname' } $borrower->{ 'surename' } ( $borrower->{'cardnumber'} ) : $circ->{ 'datetime' }\n" } );
-
- } 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.
-warn "Current issue to another member is older, returning and issuing";
- push( @output, { message => "$item->{ 'title' } ( $item->{'barcode'} ) currently issued, returning item.\n" } );
- ## AddReturnk() should be replaced with a custom function, as it will make the return date today, should be before the issue date of the current circ
- AddReturn( $circ->{ 'barcode' }, $branchcode ) unless ( DEBUG );
-
- ForceIssue( $borrower->{ 'borrowernumber' }, $item->{ 'itemnumber' }, $date_due, $branchcode, $circ->{'date'} ) 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
- }
-
+ my $circ = shift;
+
+ $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
- ForceIssue( $borrower->{ 'borrowernumber' }, $item->{ 'itemnumber' }, $date_due, $branchcode, $circ->{'date'} ) unless ( DEBUG );
- push( @output, { message => "Issued $item->{ 'title' } ( $item->{ 'barcode' } ) to $borrower->{ 'firstname' } $borrower->{ 'surename' } ( $borrower->{'cardnumber'} ) : $circ->{ 'datetime' }\n" } );
- }
}
sub kocReturnItem {
- my ( $circ ) = @_;
- ForceReturn( $circ->{'barcode'}, $circ->{'date'}, $branchcode );
-
- my $item = GetBiblioFromItemNumber( undef, $circ->{'barcode'} );
-
- ## FIXME: Is there a way to get the borrower of an item through the Koha API?
- my $sth=$dbh->prepare( "SELECT borrowernumber FROM issues WHERE itemnumber = ? AND returndate IS NULL");
- $sth->execute( $item->{'itemnumber'} );
- my ( $borrowernumber ) = $sth->fetchrow;
- $sth->finish();
-
- push( @output, { message => "Returned $item->{ 'title' } ( $item->{ 'barcode' } ) From borrower number $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( my $env, $borrower->{'borrowernumber'}, $circ->{'barcode'} );
+ 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'};
+}