RT# 42357,78190 Fix Fees appearing twice within sectioned invoices
[freeside.git] / torrus / bin / srvderive.in
1 #!@PERL@
2 #  Copyright (C) 2008  Stanislav Sinyagin
3 #
4 #  This program is free software; you can redistribute it and/or modify
5 #  it under the terms of the GNU General Public License as published by
6 #  the Free Software Foundation; either version 2 of the License, or
7 #  (at your option) any later version.
8 #
9 #  This program is distributed in the hope that it will be useful,
10 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #  GNU General Public License for more details.
13 #
14 #  You should have received a copy of the GNU General Public License
15 #  along with this program; if not, write to the Free Software
16 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
17
18 # $Id: srvderive.in,v 1.3 2011-06-11 00:44:48 ivan Exp $
19 # Stanislav Sinyagin <ssinyagin@yahoo.com>
20
21 # Combine SUM or MAX from several service IDs and create a new one
22
23 BEGIN { require '@torrus_config_pl@'; }
24
25 use strict;
26 use POSIX qw(floor);
27 use Getopt::Long;
28 use Date::Parse;
29 use Date::Format;
30 use Math::BigFloat;
31
32 use Torrus::Log;
33 use Torrus::ServiceID;
34 use Torrus::SQL::SrvExport;
35
36 my $startdate;
37 my $enddate;
38 my $onemonth;
39
40 my $function;
41 my @input;
42 my $output;
43
44 my $step = 300;
45
46 my $debug;
47 my $verbose;
48 my $help_needed;
49
50 my %known_functions = ('MAX' => \&function_max, 'SUM' => \&function_sum);
51
52
53 my $ok = GetOptions(
54                     'start=s'      => \$startdate,
55                     'end=s'        => \$enddate,
56                     'month'        => \$onemonth,
57                     'func=s'       => \$function,
58                     'in=s'         => \@input,
59                     'out=s'        => \$output,
60                     'step=i'       => \$step,
61                     'verbose'      => \$verbose,
62                     'debug'        => \$debug,
63                     'help'         => \$help_needed,
64                     );
65
66 if( $help_needed or not $ok or (not $startdate) or
67     (defined($enddate) + defined($onemonth) != 1) or
68     not $function or
69     (scalar(@input) < 2 and scalar( @ARGV ) < 2) or
70     not $output )
71 {
72     print STDERR "Usage: $0 TIMESPAN OUTPUT FUNCTION SOURCES...\n\n",
73     "TIMESPAN:\n",
74     "  --start=YYYY-MM-DD   [mandatory] Start date\n",
75     "  --month              [optional] Calendar month timespan\n",
76     "  --end=YYYY-MM-DD     [optional] Next day after the timespan end\n",
77     "  Either --month or --end option must be defined\n\n",
78     "OUTPUT:\n",    
79     "  --out=SERVICEID      [mandatory] Output Service ID\n\n",
80     "FUNCTION:\n",
81     "  --func=MAX|SUM       [mandatory] Aggregation function\n\n",
82     "SOURCES:\n",
83     "  Either --in=SERVICEID, or Service ID names as command line ",
84     "arguments.\n",
85     "  Minimum 2 sources required\n\n",
86     "Options:\n",
87     " --step=N              [300] service data interval\n",
88     " --verbose             print extra information\n",
89     " --debug               print debugging information\n",   
90     " --help                print usage information\n",   
91     "\n";
92     
93     exit 1;
94 }
95
96 push(@input, @ARGV);
97
98 if( not defined($known_functions{$function}) )
99 {
100     printf STDERR ("Unknown function: %s. Must be onne of: %s\n",
101                    $function, join(', ', sort keys %known_functions));
102     exit 1;
103 }
104
105
106
107 if( $debug )
108 {
109     Torrus::Log::setLevel('debug');
110 }
111 elsif( $verbose )
112 {
113     Torrus::Log::setLevel('verbose');
114 }
115
116
117 my $starttime = str2time( $startdate );
118 if( not defined($starttime) )
119 {
120     Error('Cannot parse start date: ' . $startdate);
121     exit 1;
122 }
123
124 # Canonize the date
125 $startdate = time2str('%Y-%m-%d', $starttime);
126
127 my $endtime;
128
129 if( defined($enddate) )
130 {
131     $endtime = str2time( $enddate ); 
132     if( not defined($endtime) )
133     {
134         Error('Cannot parse end date: ' . $enddate);
135         exit 1;
136     }
137
138     if( $endtime < $starttime )
139     {
140         Error('End date is earlier than start date');
141         exit 1;
142     }        
143 }
144 else
145 {
146     # Calculate +1 calendar month
147
148     my ($ss,$mm,$hh,$day,$month,$year,$zone) =
149         strptime( $startdate );
150     $year += 1900;
151     $month++;
152     $day++;
153   
154     my $endyear = $year;
155     my $endmonth = $month + 1;
156     
157     if( $endmonth > 12 )
158     {
159         $endmonth = 1;
160         $endyear++;
161     }
162
163     $endtime = str2time( sprintf('%.4d-%.2d-%.2d',
164                                  $endyear, $endmonth, $day) );
165     if( not defined($endtime) )
166     {
167         # oops, it was past the end of the month
168         $day++;
169         $endmonth++;
170         if( $endmonth > 12 )
171         {
172             $endmonth = 1;
173             $endyear++;
174         }
175         
176         $endtime = str2time( sprintf('%.4d-%.2d-%.2d',
177                                      $endyear, $endmonth, $day) );
178         
179         if( not defined($endtime) )
180         {
181             Error('Cannot determine the end date');
182             exit 1;
183         }
184     }
185 }
186
187 # Canonize the date
188 $enddate = time2str('%Y-%m-%d', $endtime);
189
190
191 Verbose('Start time: ', scalar(localtime($starttime)));
192 Verbose('End time: ', scalar(localtime($endtime)));
193
194 my $srvExp = Torrus::SQL::SrvExport->new();
195 if( not defined( $srvExp ) )
196 {
197     Error('Cannot connect to the SQL database');
198     exit 1;
199 }
200
201 my $srvIDs = $srvExp->getServiceIDs();
202
203 foreach my $serviceid ( @input )
204 {
205     if( not grep {$serviceid eq $_} @{$srvIDs} )
206     {
207         Error('Input service ID not found in the database: ' . $serviceid);
208         exit 1;
209     }
210 }
211
212 &Torrus::DB::setSafeSignalHandlers();
213
214 # Check if the output ServiceID exists in the local database
215 # The database contains only IDs generated from datasource trees.
216
217 my $srvIDParams = new Torrus::ServiceID();
218 if( $srvIDParams->idExists( $output ) )
219 {
220     Error('Output service ID was previously created from Torrus ' .
221           'datasource tree. Cannot override it: ' . $output);
222     exit 1;
223 }
224 &Torrus::DB::cleanupEnvironment();
225 &Torrus::DB::setUnsafeSignalHandlers();
226
227
228 my %in_data;
229
230 foreach my $serviceid ( @input )
231 {
232     $in_data{$serviceid} =
233         $srvExp->getIntervalData( $startdate, $enddate, $serviceid );
234
235     Verbose(sprintf('Loaded %d rows of data for %s',
236                     scalar( @{$in_data{$serviceid}} ),
237                     $serviceid));
238 }
239
240 my $n_points = floor( ($endtime - $starttime) / $step );
241
242 my %aligned_data;
243
244 foreach my $serviceid ( @input )
245 {
246     my @aligned = ();
247     $#aligned = $n_points;
248
249     # Fill in the aligned array. For each interval by modulo(step),
250     # we take the sum of values that get into that interval
251     
252     foreach my $row ( @{$in_data{$serviceid}} )
253     {
254         my $rowtime = str2time( $row->{'srv_date'} . 'T' .
255                                 $row->{'srv_time'} );
256         
257         my $pos = floor( ($rowtime - $starttime) / $step );
258         my $value = Math::BigFloat->new( $row->{'value'} );
259         if( $value->is_nan() )
260         {
261             $value->bzero();
262         }
263             
264         if( not defined($aligned[$pos]))
265         {
266             $aligned[$pos] = $value;
267         }
268         else
269         {
270             $aligned[$pos]->badd($value);
271         }
272     }
273
274     $aligned_data{$serviceid} = \@aligned;
275 }
276     
277 Verbose(sprintf('Aligned data into %d intervals', $n_points));
278
279 # Store the derived data
280 my $dbh = Torrus::SQL::SrvExport->dbh();
281 if( not defined( $dbh ) )
282 {
283     Error('Lost SQL connection');
284     exit 1;
285 }
286
287 my $sth = $dbh->prepare( Torrus::SQL::SrvExport->sqlInsertStatement() );
288 if( not defined( $sth ) )
289 {
290     Error('Error preparing the SQL statement: ' . $dbh->errstr);
291 }
292
293
294
295 for( my $pos = 0; $pos < $n_points; $pos++ )
296 {
297     my @args;
298     foreach my $serviceid ( @input )
299     {
300         my $val = $aligned_data{$serviceid}->[$pos];
301         if( defined( $val ) )
302         {
303             push( @args, $val );
304         }
305     }
306
307     if( scalar( @args ) > 0 )
308     {
309         my $value = &{$known_functions{$function}}(@args);
310
311         my $timestamp = $starttime + $pos * $step;
312         my $datestr = time2str('%Y-%m-%d', $timestamp);
313         my $timestr = time2str('%H:%M:%S', $timestamp);
314         
315         if( isDebug() )
316         {
317             Debug('Updating SQL database: ' .
318                   join(', ', $datestr, $timestr,
319                        $output, $value, $step ));
320         }
321     
322         if( not $sth->execute( $datestr, $timestr,
323                                $output, $value, $step ) )
324         {
325             Error('Error executing SQL: ' . $dbh->errstr);
326             exit 1;
327         }
328     }
329 }
330
331 $dbh->commit or Error($dbh->errstr);
332
333 Verbose('Database update finished');
334
335 exit($ok ? 0:1);
336
337
338 sub function_max
339 {
340     my $value = Math::BigFloat->new();
341     $value->binf('-');
342
343     foreach my $a ( @_ )
344     {
345         if( $value->bcmp($a) < 0 )
346         {
347             $value = Math::BigFloat->new($a);
348         }
349     }
350
351     return $value;
352 }
353
354
355 sub function_sum
356 {
357     my $value = Math::BigFloat->new(0);
358
359     foreach my $a ( @_ )
360     {
361         $value->badd($a);
362     }
363
364     return $value;
365 }
366     
367
368
369 # Local Variables:
370 # mode: perl
371 # indent-tabs-mode: nil
372 # perl-indent-level: 4
373 # End: