eb11861f8dc7f83f17529607b1fe876a14c9118f
[koha.git] / misc / cronjobs / update_totalissues.pl
1 #!/usr/bin/perl
2
3 # Copyright 2012 C & P Bibliography Services
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 use strict;
21 use warnings;
22
23 BEGIN {
24
25     # find Koha's Perl modules
26     # test carefully before changing this
27     use FindBin;
28     eval { require "$FindBin::Bin/../kohalib.pl" };
29 }
30
31 use Getopt::Long;
32 use Pod::Usage;
33
34 use Koha::Script -cron;
35 use C4::Context;
36 use C4::Biblio;
37 use C4::Log;
38 use DateTime;
39 use DateTime::Format::MySQL;
40 use Time::HiRes qw/time/;
41 use POSIX qw/strftime ceil/;
42
43 sub usage {
44     pod2usage( -verbose => 2 );
45     exit;
46 }
47
48 $| = 1;
49
50 # command-line parameters
51 my $verbose   = 0;
52 my $test_only = 0;
53 my $want_help = 0;
54 my $since;
55 my $interval;
56 my $usestats    = 0;
57 my $useitems    = 0;
58 my $incremental = 0;
59 my $commit      = 100;
60 my $unit;
61
62 my $result = GetOptions(
63     'v|verbose'    => \$verbose,
64     't|test'       => \$test_only,
65     's|since=s'    => \$since,
66     'i|interval=s' => \$interval,
67     'use-stats'    => \$usestats,
68     'use-items'    => \$useitems,
69     'incremental'  => \$incremental,
70     'c|commit=i'   => \$commit,
71     'h|help'       => \$want_help
72 );
73
74 binmode( STDOUT, ":utf8" );
75
76 if ( defined $since && defined $interval ) {
77     print "The --since and --interval options are mutually exclusive.\n\n";
78     $want_help = 1;
79 }
80
81 if ( $useitems && $incremental ) {
82     print
83       "The --use-items and --incremental options are mutually exclusive.\n\n";
84     $want_help = 1;
85 }
86
87 if ( $incremental && !( defined $since || defined $interval ) ) {
88     $interval = '24h';
89 }
90
91 unless ( $usestats || $useitems ) {
92     print "You must specify either --use-stats and/or --use-items.\n\n";
93     $want_help = 1;
94 }
95
96 if ( not $result or $want_help ) {
97     usage();
98 }
99
100 cronlogaction();
101
102 my $dbh = C4::Context->dbh;
103 $dbh->{AutoCommit} = 0;
104
105 my $num_bibs_processed = 0;
106 my $num_bibs_error = 0;
107
108 my $starttime = time();
109
110 process_items() if $useitems;
111 process_stats() if $usestats;
112
113 report();
114
115 exit 0;
116
117 sub process_items {
118     my $query =
119 "SELECT items.biblionumber, SUM(items.issues) FROM items GROUP BY items.biblionumber;";
120     process_query($query);
121 }
122
123 sub process_stats {
124     if ($interval) {
125         my $dt = DateTime->now;
126
127         my %units = (
128             h => 'hours',
129             d => 'days',
130             w => 'weeks',
131             m => 'months',
132             y => 'years'
133         );
134
135         $interval =~ m/([0-9]*)([hdwmy]?)$/;
136         $unit = $2 || 'd';
137         $since = DateTime::Format::MySQL->format_datetime(
138             $dt->subtract( $units{$unit} => $1 ) );
139     }
140     my $limit = '';
141     $limit = " AND statistics.datetime >= ?" if ( $interval || $since );
142
143     my $query =
144 "SELECT biblio.biblionumber, COUNT(statistics.itemnumber) FROM biblio LEFT JOIN items ON (biblio.biblionumber=items.biblionumber) LEFT JOIN statistics ON (items.itemnumber=statistics.itemnumber) WHERE statistics.type = 'issue' $limit GROUP BY biblio.biblionumber;";
145     process_query( $query, $limit );
146
147     unless ($incremental) {
148         $query =
149 "SELECT biblio.biblionumber, 0 FROM biblio LEFT JOIN items ON (biblio.biblionumber=items.biblionumber) LEFT JOIN statistics ON (items.itemnumber=statistics.itemnumber) WHERE statistics.itemnumber IS NULL GROUP BY biblio.biblionumber;";
150         process_query( $query, '' );
151
152         $query =
153 "SELECT biblio.biblionumber, 0 FROM biblio LEFT JOIN items ON (biblio.biblionumber=items.biblionumber) WHERE items.itemnumber IS NULL GROUP BY biblio.biblionumber;";
154         process_query( $query, '' );
155     }
156
157     $dbh->commit();
158 }
159
160 sub process_query {
161     my $query    = shift;
162     my $uselimit = shift;
163     my $sth      = $dbh->prepare($query);
164
165     if ( $since && $uselimit ) {
166         $sth->execute($since);
167     }
168     else {
169         $sth->execute();
170     }
171
172     while ( my ( $biblionumber, $totalissues ) = $sth->fetchrow_array() ) {
173         $num_bibs_processed++;
174         $totalissues = 0 unless $totalissues;
175         print "Processing bib $biblionumber ($totalissues issues)\n"
176           if $verbose;
177         if ( not $test_only ) {
178             my $ret;
179             if ( $incremental && $totalissues > 0 ) {
180                 $ret = UpdateTotalIssues( $biblionumber, $totalissues );
181             }
182             else {
183                 $ret = UpdateTotalIssues( $biblionumber, 0, $totalissues );
184             }
185             unless ($ret) {
186                 print "Error while processing bib $biblionumber\n" if $verbose;
187                 $num_bibs_error++;
188             }
189         }
190         if ( not $test_only and ( $num_bibs_processed % $commit ) == 0 ) {
191             print_progress_and_commit($num_bibs_processed);
192         }
193     }
194
195     $dbh->commit();
196 }
197
198 sub report {
199     my $endtime = time();
200     my $totaltime = ceil( ( $endtime - $starttime ) * 1000 );
201     $starttime = strftime( '%D %T', localtime($starttime) );
202     $endtime   = strftime( '%D %T', localtime($endtime) );
203
204     my $summary = <<_SUMMARY_;
205
206 Update total issues count script report
207 =======================================================
208 Run started at:                         $starttime
209 Run ended at:                           $endtime
210 Total run time:                         $totaltime ms
211 Number of bibs modified:                $num_bibs_processed
212 Number of bibs with error:              $num_bibs_error
213 _SUMMARY_
214     $summary .= "\n****  Ran in test mode only  ****\n" if $test_only;
215     print $summary;
216 }
217
218 sub print_progress_and_commit {
219     my $recs = shift;
220     $dbh->commit();
221     print "... processed $recs records\n";
222 }
223
224 =head1 NAME
225
226 update_totalissues.pl
227
228 =head1 SYNOPSIS
229
230   update_totalissues.pl --use-stats
231   update_totalissues.pl --use-items
232   update_totalissues.pl --commit=1000
233   update_totalissues.pl --since='2012-01-01'
234   update_totalissues.pl --interval=30d
235
236 =head1 DESCRIPTION
237
238 This batch job populates bibliographic records' total issues count based
239 on historical issue statistics.
240
241 =over 8
242
243 =item B<--help>
244
245 Prints this help
246
247 =item B<-v|--verbose>
248
249 Provide verbose log information (list every bib modified).
250
251 =item B<--use-stats>
252
253 Use the data in the statistics table for populating total issues.
254
255 =item B<--use-items>
256
257 Use items.issues data for populating total issues. Note that issues
258 data from the items table does not respect the --since or --interval
259 options, by definition. Also note that if both --use-stats and
260 --use-items are specified, the count of biblios processed will be
261 misleading.
262
263 =item B<-s|--since=DATE>
264
265 Only process issues recorded in the statistics table since DATE.
266
267 =item B<-i|--interval=S>
268
269 Only process issues recorded in the statistics table in the last N
270 units of time. The interval should consist of a number with a one-letter
271 unit suffix. The valid suffixes are h (hours), d (days), w (weeks),
272 m (months), and y (years). The default unit is days.
273
274 =item B<--incremental>
275
276 Add the number of issues found in the statistics table to the existing
277 total issues count. Intended so that this script can be used as a cron
278 job to update popularity information during low-usage periods. If neither
279 --since or --interval are specified, incremental mode will default to
280 processing the last twenty-four hours.
281
282 =item B<--commit=N>
283
284 Commit the results to the database after every N records are processed.
285
286 =item B<--test>
287
288 Only test the popularity population script.
289
290 =back
291
292 =head1 WARNING
293
294 If the time on your database server does not match the time on your Koha
295 server you will need to take that into account, and probably use the
296 --since argument instead of the --interval argument for incremental
297 updating.
298
299 =head1 CREDITS
300
301 This patch to Koha was sponsored by the Arcadia Public Library and the
302 Arcadia Public Library Foundation in honor of Jackie Faust-Moreno, late
303 director of the Arcadia Public Library.
304
305 =head1 AUTHOR
306
307 Jared Camins-Esakov <jcamins AT cpbibliography DOT com>
308
309 =cut