Bug 17600: Standardize our EXPORT_OK
[koha.git] / offline_circ / process_koc.pl
1 #!/usr/bin/perl
2
3 # 2008 Kyle Hall <kyle.m.hall@gmail.com>
4
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19 #
20
21 use Modern::Perl;
22
23 use CGI qw ( -utf8 );
24
25 use C4::Output qw( output_html_with_http_headers );
26 use C4::Auth qw( get_template_and_user );
27 use C4::Context;
28 use C4::Accounts;
29 use C4::Circulation qw( barcodedecode GetOpenIssue AddRenewal AddIssue MarkIssueReturned );
30 use C4::Items qw( ModDateLastSeen );
31 use C4::Members;
32 use C4::Stats;
33 use C4::BackgroundJob;
34 use Koha::UploadedFiles;
35 use Koha::Account;
36 use Koha::Checkouts;
37 use Koha::Patrons;
38
39 use Date::Calc qw( Date_to_Days );
40
41 use constant DEBUG => 0;
42
43 # this is the file version number that we're coded against.
44 my $FILE_VERSION = '1.0';
45
46 our $query = CGI->new;
47
48 my ($template, $loggedinuser, $cookie) = get_template_and_user({
49     template_name => "offline_circ/process_koc.tt",
50     query => $query,
51     type => "intranet",
52      flagsrequired   => { circulate => "circulate_remaining_permissions" },
53 });
54
55
56 my $fileID=$query->param('uploadedfileid');
57 my $runinbackground = $query->param('runinbackground');
58 my $completedJobID = $query->param('completedJobID');
59 my %cookies = parse CGI::Cookie($cookie);
60 my $sessionID = $cookies{'CGISESSID'}->value;
61 ## 'Local' globals.
62 our $dbh = C4::Context->dbh();
63 our @output = (); ## For storing messages to be displayed to the user
64
65
66 if ($completedJobID) {
67     my $job = C4::BackgroundJob->fetch($sessionID, $completedJobID);
68     my $results = $job->results();
69     $template->param(transactions_loaded => 1);
70     $template->param(messages => $results->{results});
71 } elsif ($fileID) {
72     my $upload = Koha::UploadedFiles->find( $fileID );
73     my $fh = $upload? $upload->file_handle: undef;
74     my $filename = $upload? $upload->filename: undef;
75     my @input_lines = $fh? <$fh>: ();
76     $fh->close if $fh;
77
78     my $job = undef;
79
80     if ($runinbackground) {
81         my $job_size = scalar(@input_lines);
82         $job = C4::BackgroundJob->new($sessionID, $filename, '/cgi-bin/koha/offline_circ/process_koc.pl', $job_size);
83         my $jobID = $job->id();
84
85         # fork off
86         if (my $pid = fork) {
87             # parent
88             # return job ID as JSON
89
90             # prevent parent exiting from
91             # destroying the kid's database handle
92             # FIXME: according to DBI doc, this may not work for Oracle
93             $dbh->{InactiveDestroy}  = 1;
94
95             my $reply = CGI->new("");
96             print $reply->header(-type => 'text/html');
97             print '{"jobID":"' . $jobID . '"}';
98             exit 0;
99         } elsif (defined $pid) {
100             # child
101             # close STDOUT to signal to Apache that
102             # we're now running in the background
103             close STDOUT;
104             close STDERR;
105         } else {
106             # fork failed, so exit immediately
107             # fork failed, so exit immediately
108             warn "fork failed while attempting to run offline_circ/process_koc.pl as a background job";
109             exit 0;
110         }
111
112         # if we get here, we're a child that has detached
113         # itself from Apache
114
115     }
116
117     my $header_line = shift @input_lines;
118     my $file_info   = parse_header_line($header_line);
119     if ($file_info->{'Version'} ne $FILE_VERSION) {
120         push @output, {
121             message => 1,
122             ERROR_file_version => 1,
123             upload_version => $file_info->{'Version'},
124             current_version => $FILE_VERSION
125         };
126     }
127
128     my $i = 0;
129     foreach  my $line (@input_lines)  {
130         $i++;
131         my $command_line = parse_command_line($line);
132
133         # map command names in the file to subroutine names
134         my %dispatch_table = (
135             issue     => \&kocIssueItem,
136             'return'  => \&kocReturnItem,
137             payment   => \&kocMakePayment,
138         );
139
140         # call the right sub name, passing the hashref of command_line to it.
141         if ( exists $dispatch_table{ $command_line->{'command'} } ) {
142             $dispatch_table{ $command_line->{'command'} }->($command_line);
143         } else {
144             warn "unknown command: '$command_line->{command}' not processed";
145         }
146
147         if ($runinbackground) {
148             $job->progress($i);
149         }
150     }
151
152     if ($runinbackground) {
153         $job->finish({ results => \@output }) if defined($job);
154     } else {
155         $template->param(transactions_loaded => 1);
156         $template->param(messages => \@output);
157     }
158 }
159
160 output_html_with_http_headers $query, $cookie, $template->output;
161
162 =head1 FUNCTIONS
163
164 =head2 parse_header_line
165
166 parses the header line from a .koc file. This is the line that
167 specifies things such as the file version, and the name and version of
168 the offline circulation tool that generated the file. See
169 L<http://wiki.koha-community.org/wiki/Koha_offline_circulation_file_format>
170 for more information.
171
172 pass in a string containing the header line (the first line from th
173 file).
174
175 returns a hashref containing the information from the header.
176
177 =cut
178
179 sub parse_header_line {
180     my $header_line = shift;
181     chomp($header_line);
182     $header_line =~ s/\r//g;
183
184     my @fields = split( /\t/, $header_line );
185     my %header_info = map { split( /=/, $_ ) } @fields;
186     return \%header_info;
187 }
188
189 =head2 parse_command_line
190
191 =cut
192
193 sub parse_command_line {
194     my $command_line = shift;
195     chomp($command_line);
196     $command_line =~ s/\r//g;
197
198     my ( $timestamp, $command, @args ) = split( /\t/, $command_line );
199     my ( $date,      $time,    $id )   = split( /\s/, $timestamp );
200
201     my %command = (
202         date    => $date,
203         time    => $time,
204         id      => $id,
205         command => $command,
206     );
207
208     # set the rest of the keys using a hash slice
209     my $argument_names = arguments_for_command($command);
210     @command{@$argument_names} = @args;
211
212     return \%command;
213
214 }
215
216 =head2 arguments_for_command
217
218 fetches the names of the columns (and function arguments) found in the
219 .koc file for a particular command name. For instance, the C<issue>
220 command requires a C<cardnumber> and C<barcode>. In that case this
221 function returns a reference to the list C<qw( cardnumber barcode )>.
222
223 parameters: the command name
224
225 returns: listref of column names.
226
227 =cut
228
229 sub arguments_for_command {
230     my $command = shift;
231
232     # define the fields for this version of the file.
233     my %format = (
234         issue   => [qw( cardnumber barcode )],
235         return  => [qw( barcode )],
236         payment => [qw( cardnumber amount )],
237     );
238
239     return $format{$command};
240 }
241
242 sub kocIssueItem {
243     my $circ = shift;
244
245     $circ->{ 'barcode' } = barcodedecode($circ->{'barcode'}) if( $circ->{'barcode'} && C4::Context->preference('itemBarcodeInputFilter'));
246     my $branchcode = C4::Context->userenv->{branch};
247     my $patron = Koha::Patrons->find( { cardnumber => $circ->{cardnumber} } );
248     my $borrower = $patron->unblessed;
249     my $item = Koha::Items->find({ barcode => $circ->{barcode} });
250     my $issue = Koha::Checkouts->find( { itemnumber => $item->itemnumber } );
251     my $biblio = $item->biblio;
252
253     if ( $issue ) { ## Item is currently checked out to another person.
254         #warn "Item Currently Issued.";
255         my $issue = GetOpenIssue( $item->itemnumber ); # FIXME Hum? That does not make sense, if it's in the issue table, the issue is open (i.e. returndate is null)
256
257         if ( $issue->{'borrowernumber'} eq $borrower->{'borrowernumber'} ) { ## Issued to this person already, renew it.
258             #warn "Item issued to this member already, renewing.";
259
260             C4::Circulation::AddRenewal(
261                 $issue->{'borrowernumber'},    # borrowernumber
262                 $item->itemnumber,             # itemnumber
263                 undef,                         # branch
264                 undef,                         # datedue - let AddRenewal calculate it automatically
265                 $circ->{'date'},               # issuedate
266             ) unless (DEBUG);
267
268             push @output, {
269                 renew => 1,
270                 title => $biblio->title,
271                 biblionumber => $biblio->biblionumber,
272                 barcode => $item->barcode,
273                 firstname => $borrower->{ 'firstname' },
274                 surname => $borrower->{ 'surname' },
275                 borrowernumber => $borrower->{'borrowernumber'},
276                 cardnumber => $borrower->{'cardnumber'},
277                 datetime => $circ->{ 'datetime' }
278             };
279
280         } else {
281             #warn "Item issued to a different member.";
282             #warn "Date of previous issue: $issue->{'issuedate'}";
283             #warn "Date of this issue: $circ->{'date'}";
284             my ( $i_y, $i_m, $i_d ) = split( /-/, $issue->{'issuedate'} );
285             my ( $c_y, $c_m, $c_d ) = split( /-/, $circ->{'date'} );
286
287             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.
288                 C4::Circulation::AddIssue( $borrower, $circ->{'barcode'}, undef, undef, $circ->{'date'} ) unless ( DEBUG );
289                 push @output, {
290                     issue => 1,
291                     title => $biblio->title,
292                     biblionumber => $biblio->biblionumber,
293                     barcode => $item->barcode,
294                     firstname => $borrower->{ 'firstname' },
295                     surname => $borrower->{ 'surname' },
296                     borrowernumber => $borrower->{'borrowernumber'},
297                     cardnumber => $borrower->{'cardnumber'},
298                     datetime => $circ->{ 'datetime' }
299                 };
300
301             } 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.
302                 #warn "Current issue to another member is newer. Doing nothing";
303                 ## This situation should only happen of the Offline Circ data is *really* old.
304                 ## FIXME: write line to old_issues and statistics
305             }
306         }
307     } else { ## Item is not checked out to anyone at the moment, go ahead and issue it
308         C4::Circulation::AddIssue( $borrower, $circ->{'barcode'}, undef, undef, $circ->{'date'} ) unless ( DEBUG );
309         push @output, {
310             issue => 1,
311             title => $biblio->title,
312             biblionumber => $biblio->biblionumber,
313             barcode => $item->barcode,
314             firstname => $borrower->{ 'firstname' },
315             surname => $borrower->{ 'surname' },
316             borrowernumber => $borrower->{'borrowernumber'},
317             cardnumber => $borrower->{'cardnumber'},
318             datetime =>$circ->{ 'datetime' }
319         };
320     }
321 }
322
323 sub kocReturnItem {
324     my ( $circ ) = @_;
325     $circ->{'barcode'} = barcodedecode($circ->{'barcode'}) if( $circ->{'barcode'} && C4::Context->preference('itemBarcodeInputFilter'));
326     my $item = Koha::Items->find({ barcode => $circ->{barcode} });
327     my $biblio = $item->biblio;
328     my $borrowernumber = _get_borrowernumber_from_barcode( $circ->{'barcode'} );
329     if ( $borrowernumber ) {
330         my $patron = Koha::Patrons->find( $borrowernumber );
331         C4::Circulation::MarkIssueReturned(
332             $borrowernumber,
333             $item->itemnumber,
334             $circ->{'date'},
335             $patron->privacy
336         );
337
338         $item->onloan(undef)->store;
339         ModDateLastSeen( $item->itemnumber );
340
341         push @output,
342           {
343             return         => 1,
344             title          => $biblio->title,
345             biblionumber   => $biblio->biblionumber,
346             barcode        => $item->barcode,
347             borrowernumber => $patron->borrowernumber,
348             firstname      => $patron->firstname,
349             surname        => $patron->surname,
350             cardnumber     => $patron->cardnumber,
351             datetime       => $circ->{'datetime'}
352           };
353     } else {
354         push @output, {
355             ERROR_no_borrower_from_item => 1,
356             badbarcode => $circ->{'barcode'}
357         };
358     }
359 }
360
361 sub kocMakePayment {
362     my ($circ) = @_;
363
364     my $cardnumber = $circ->{cardnumber};
365     my $amount = $circ->{amount};
366
367     my $patron = Koha::Patrons->find( { cardnumber => $cardnumber } );
368
369     Koha::Account->new( { patron_id => $patron->id } )
370       ->pay( { amount => $amount, interface => C4::Context->interface } );
371
372     push @output,
373       {
374         payment    => 1,
375         amount     => $circ->{'amount'},
376         firstname  => $patron->firstname,
377         surname    => $patron->surname,
378         cardnumber => $patron->cardnumber,
379         borrower   => $patron->id,
380       };
381 }
382
383 =head2 _get_borrowernumber_from_barcode
384
385 pass in a barcode
386 get back the borrowernumber of the patron who has it checked out.
387 undef if that can't be found
388
389 =cut
390
391 sub _get_borrowernumber_from_barcode {
392     my $barcode = shift;
393
394     return unless $barcode;
395
396     my $item = Koha::Items->find({ barcode => $barcode });
397     return unless $item;
398
399     my $issue = Koha::Checkouts->find( { itemnumber => $item->itemnumber } );
400     return unless $issue;
401     return $issue->borrowernumber;
402 }