bug 2503: refactoring and bugfixing on create_koc_db.pl
[koha.git] / misc / cronjobs / create_koc_db.pl
1 #!/usr/bin/perl -w
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 under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 2 of the License, or (at your option) any later
10 # version.
11 #
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along with
17 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
18 # Suite 330, Boston, MA  02111-1307 USA
19 #
20
21 =head1 NAME
22
23 create_koc_db.pl - create a database file for the offline circulation tool
24
25 =head1 SYNOPSIS
26
27 create_koc_db.pl
28
29  Options:
30    -help                          brief help message
31    -man                           full documentation
32
33 =head1 OPTIONS
34
35 =over 8
36
37 =item B<--help>
38
39 Print a brief help message and exits.
40
41 =item B<--man>
42
43 Prints the manual page and exits.
44
45 =item B<--file>
46
47 the filename that we should use for the database file that we produce. Defaults to "borrowers.db"
48
49 =item B<--force>
50
51 Forcefully overwrite any existing db file. Defaults to false, so
52 program will terminate prematurely if the file already exists.
53
54 =back
55
56 =head1 DESCRIPTION
57
58 This script generates a sqlite database file full of patron and
59 holdings data that can be used by an offline circulation tool.
60
61 =head1 USAGE EXAMPLES
62
63 This program could be run from cron to occasionally refresh the
64 offline circulation database. For instance:
65
66 C<0 0 * * * create_koc_db.pl>
67
68 =head1 SEE ALSO
69
70 This program was originally created to interact with Kyle Hall's
71 offline circulation tool, which is available from
72 L<http://kylehall.info/index.php/projects/koha-tools/koha-offline-circulation/>,
73 but any similar tool could use the database that is produced.
74
75 =cut
76
77 $|++;
78
79 use strict;
80 use warnings;
81
82 use DBI;
83 use Getopt::Long;
84 use Pod::Usage;
85 use C4::Context;
86 use English qw(-no_match_vars);
87
88 my $verbose  = 0;
89 my $help     = 0;
90 my $man      = 0;
91 my $filename = 'borrowers.db';
92 my $force    = 0;
93
94 GetOptions(
95     'verbose' => \$verbose,
96     'help'    => \$help,
97     'man'     => \$man,
98     'file=s'  => \$filename,
99     'force'   => \$force,
100 ) or pod2usage(2);
101 pod2usage(1) if $help;
102 pod2usage( -verbose => 2 ) if $man;
103
104 prepare_file_for_writing($filename)
105   or die "file: '$filename' already exists. Use --force to overwrite\n";
106
107 verify_dbd_sqlite();
108
109 ## Create DB Connections
110 my $dbh_mysql = C4::Context->dbh;
111 my $dbh_sqlite = DBI->connect( "dbi:SQLite:dbname=$filename", "", "" );
112
113 create_borrowers_table();
114 populate_borrowers_table();
115
116 create_issues_table();
117 populate_issues_table();
118
119 =head1 INTERNAL METHODS
120
121 =head2 verify_dbd_sqlite
122
123 Since DBD::SQLite is a new prerequisite and an optional one, let's
124 make sure we have a new enough version of it.
125
126 =cut
127
128 sub verify_dbd_sqlite {
129
130     eval { require DBD::SQLite; };
131     if ( $EVAL_ERROR ) {
132         my $msg = <<'END_MESSAGE';
133 DBD::SQLite is required to generate offline circultion database files, but not found.
134 Please install the DBD::SQLite perl module. It is availalbe from
135 http://search.cpan.org/dist/DBD-SQLite/ or through the CPAN module.
136 END_MESSAGE
137         die $msg;
138     }
139 }
140
141 =head2 prepare_file_for_writing
142
143 pass in the filename that we're considering using for the SQLite db.
144
145 returns true if we can use it.
146
147 returns false if we can't. For example, if it alredy exists and we
148 don't have --force or don't have permissions to unlink it.
149
150 =cut
151
152 sub prepare_file_for_writing {
153     my $filename = shift;
154     if ( -e $filename ) {
155
156         # this file exists. remove it if --force.
157         if ($force) {
158             return unlink $filename;
159         } else {
160             return;
161         }
162     }
163     return $filename;
164 }
165
166 =head2 create_borrowers_table
167
168 Create sqlite borrowers table to mirror the koha borrowers table structure
169
170 =cut
171
172 sub create_borrowers_table {
173
174     my %borrowers_info = get_columns_and_types_of_table( 'borrowers' );
175     my $sqlite_create_sql = "CREATE TABLE borrowers ( \n";
176     
177     $sqlite_create_sql .= join(',', map{ $_ . ' ' . $borrowers_info{$_} } keys %borrowers_info);
178     
179     $sqlite_create_sql .= " , \n total_fines decimal(28,6) ";    ## Extra field to store the total fines for a borrower in.
180     $sqlite_create_sql .= " ) ";
181
182     my $return = $dbh_sqlite->do($sqlite_create_sql);
183     unless ( $return ) {
184         warn 'unable to create borrowers table: ' . $dbh_sqlite->errstr();
185     }
186     return $return;
187
188 }
189
190 =head2 populate_borrowers_table
191
192 Import the data from the koha.borrowers table into our sqlite table
193
194 =cut
195
196 sub populate_borrowers_table {
197
198     my @borrower_fields = get_columns_of_table( 'borrowers' );
199     push @borrower_fields, 'total_fines';
200     
201     my $sql = "INSERT INTO borrowers ( ";
202     $sql .= join( ',', @borrower_fields );
203     $sql .= " ) VALUES ( ";
204     $sql .= join( ',', map { '?' } @borrower_fields );
205     $sql .= " ) ";
206     my $sth_sqlite = $dbh_sqlite->prepare($sql);
207
208     my $sth_mysql    = $dbh_mysql->prepare(<<'END_SQL');
209 SELECT borrowers.borrowernumber, sum( accountlines.amountoutstanding ) as total_fines
210 FROM borrowers
211 LEFT JOIN accountlines
212   ON borrowers.borrowernumber = accountlines.borrowernumber
213 GROUP BY borrowernumber;
214 END_SQL
215
216     my $fields_count = $sth_mysql->execute();
217     warn "preparing to insert $fields_count borrowers\n" if $verbose;
218
219     my $count;
220     while ( my $borrower = $sth_mysql->fetchrow_hashref ) {
221         $count++;
222         if ( $verbose ) {
223             print '.' unless ( $count % 10 );
224             print "$count\n" unless ( $count % 1000 );
225         }
226         $sth_sqlite->execute( @$borrower{ @borrower_fields } );
227         $sth_sqlite->finish();
228     }
229     print "inserted $count borrowers\n" if $verbose;
230     # add_fines_to_borrowers_table();
231 }
232
233 =head2 add_fines_to_borrowers_table
234
235 Import the fines from koha.accountlines into the sqlite db
236
237 =cut
238
239 sub add_fines_to_borrowers_table {
240
241     print "preparing to update borrowers\n" if $verbose;
242     my $sth_mysql = $dbh_mysql->prepare(
243         "SELECT DISTINCT borrowernumber, SUM( amountoutstanding ) AS total_fines
244                                     FROM accountlines
245                                     GROUP BY borrowernumber"
246     );
247     $sth_mysql->execute();
248     my $count;
249     while ( my $result = $sth_mysql->fetchrow_hashref() ) {
250         $count++;
251         if ( $verbose ) {
252             print '.' unless ( $count % 10 );
253             print "$count\n" unless ( $count % 1000 );
254         }
255
256         my $borrowernumber = $result->{'borrowernumber'};
257         my $total_fines    = $result->{'total_fines'};
258
259         # warn "Fines for Borrower # $borrowernumber are \$ $total_fines \n" if $verbose;
260         my $sql = "UPDATE borrowers SET total_fines = ? WHERE borrowernumber = ?";
261
262         my $sth_sqlite = $dbh_sqlite->prepare($sql);
263         $sth_sqlite->execute( $total_fines, $borrowernumber );
264         $sth_sqlite->finish();
265     }
266     print "updated $count borrowers\n" if ( $verbose && $count );
267 }
268
269 =head2 create_issue_table
270
271 Create sqlite issues table with minimal information needed from koha tables issues, items, biblio, biblioitems
272
273 =cut
274
275 sub create_issues_table {
276
277     my $fields = get_columns_for_issues_table();
278
279     my $sqlite_create_sql = "CREATE TABLE issues ( \n";
280
281     my $firstField = 1;
282     foreach my $key ( keys %$fields ) {
283         my $field = $key;
284         my $type  = $fields->{$key};
285
286         if ($firstField) {
287             $sqlite_create_sql .= " $field $type ";
288             $firstField = 0;
289         } else {
290             $sqlite_create_sql .= ", \n $field $type ";
291         }
292     }
293     $sqlite_create_sql .= " ) ";
294
295     my $sth_sqlite = $dbh_sqlite->prepare($sqlite_create_sql);
296     $sth_sqlite->execute();
297     $sth_sqlite->finish();
298
299 }
300
301 =head2 populate_issues_table
302
303 Import open issues from the koha database
304
305 =cut
306
307 sub populate_issues_table {
308
309     print "preparing to populate ISSUES table\n" if $verbose;
310     my $sth_mysql = $dbh_mysql->prepare(
311         "SELECT * FROM issues, items, biblioitems, biblio
312                                     WHERE issues.itemnumber = items.itemnumber
313                                     AND items.biblionumber = biblioitems.biblionumber
314                                     AND items.biblionumber = biblio.biblionumber
315                                     AND returndate IS NULL"
316     );
317     $sth_mysql->execute();
318
319     my $column_names = $sth_mysql->{'NAME'};
320     my $sql_sqlite = "INSERT INTO issues ( ";
321     $sql_sqlite .= join( ',', @$column_names );
322     $sql_sqlite .= " ) VALUES ( ";
323     $sql_sqlite .= join( ',', map { '?' } @$column_names );
324     $sql_sqlite .= " ) ";
325
326     warn "$sql_sqlite\n" if $verbose;
327     my $sth_sqlite = $dbh_sqlite->prepare($sql_sqlite);
328
329     my $count;
330     while ( my $result = $sth_mysql->fetchrow_hashref ) {
331
332         $count++;
333         if ( $verbose ) {
334             print '.' unless ( $count % 10 );
335             print "$count\n" unless ( $count % 1000 );
336         }
337
338         $sth_sqlite->execute( @$result{ @$column_names } );
339         $sth_sqlite->finish();
340     }
341     print "inserted $count issues\n" if ( $verbose && $count );
342 }
343
344 =head2 get_columns_of_table
345
346 pass in the name of a database table.
347
348 returns list of column names in that table.
349
350 =cut
351
352 sub get_columns_of_table {
353     my $table_name = shift;
354
355     my %column_info = get_columns_and_types_of_table( $table_name );
356     my @columns = keys %column_info;
357     return @columns;
358
359 }
360
361 =head2 get_columns_and_types_of_table
362
363 pass in the name of a database table
364
365 returns a hash of column names to their types.
366
367 =cut
368
369 sub get_columns_and_types_of_table {
370     my $table_name = shift;
371
372     my $column_info = $dbh_mysql->selectall_arrayref( "SHOW COLUMNS FROM $table_name" );
373     my %columns = map{ $_->[0] => $_->[1] } @$column_info;
374     return %columns;
375
376 }
377
378 =head2 get_columns_for_issues_table
379
380 This sub returns a hashref where the keys are all the fields in the given tables, and the data is the field's type
381
382 =cut
383
384 sub get_columns_for_issues_table {
385   
386   my @tables = ( 'issues', 'items', 'biblioitems', 'biblio' );
387
388   my %fields;
389   
390   foreach my $table ( @tables ) {
391       my %table_info = get_columns_and_types_of_table( $table );
392       %fields = ( %fields, %table_info );
393   }
394   return \%fields;
395 }
396
397 1;
398 __END__
399