846b50c2c58967acc7822a66f314afad16d40365
[freeside.git] / httemplate / search / sqlradius_usage.html
1 % if ( @include_agents ) {
2 %   # jumbo report
3 <& /elements/header.html, $title &>
4 %   foreach my $agent ( @include_agents ) {
5 % $cgi->param('agentnum', $agent->agentnum); #for download links
6 <DIV WIDTH="100%" STYLE="page-break-after: always">
7 <FONT SIZE=6><% $agent->agent %></FONT><BR><BR>
8   <& cust_pkg_sqlradius_usage.html, 
9       exports           => @exports,
10       agentnum          => $agent->agentnum,
11       nohtmlheader      => 1,
12       download_label    => 'Download this section',
13       &>
14 </DIV>
15 <BR><BR>
16 %  }
17 <& /elements/footer.html &>
18 % } else {
19 <& elements/search.html,
20   'title'       => $title,
21   'name'        => $combine_svcs ? 'packages' : 'services',
22   'query'       => $sql_query,
23   'count_query' => $count_query,
24   'header'      => [ #FS::UI::Web::cust_header(),
25                      '#',
26                      'Customer',
27                      'Package',
28                      'User',
29                      'Upload (GB)',
30                      'Download (GB)',
31                      'Total (GB)',
32                    ],
33   'footer'      => \@footer,
34   'fields'      => [ #\&FS::UI::Web::cust_fields,
35                      'display_custnum',
36                      'name',
37                      'pkg',
38                      @pkg_usage, # username, upload, download, total
39                    ],
40   'order_by_sql' => $order_by_sql,
41   'links'       => [ #( map { $_ ne 'Cust. Status' ? $link_cust : '' }
42                      #  FS::UI::Web::cust_header() ),
43                      $link_cust,
44                      $link_cust,
45                      $link_pkg,
46                      $link_svc,
47                      '',
48                      '',
49                    ],
50   'align'       => #FS::UI::Web::cust_aligns() .
51                    'rlcrrr',
52   'nohtmlheader'    => ($opt{'nohtmlheader'} || 0),
53   'download_label'  => $opt{'download_label'},
54 &>
55 % }
56 <%init>
57
58 my %opt = @_;
59
60 my $curuser = $FS::CurrentUser::CurrentUser;
61 die "access denied" unless $curuser->access_right('List packages');
62
63 my $title = 'Data Usage Report - '; 
64 my $agentnum = '';
65 my @include_agents = ();
66
67 my $combine_svcs = $cgi->param('combine_svcs') ? 1 : 0;
68
69 if ( $opt{'agentnum'} =~ /^(\d+)$/ ) {
70   $agentnum = $opt{'agentnum'};
71 } else {
72
73   my @agentnums = grep /^(\d+)$/, $cgi->param('agentnum');
74
75   if ( ! @agentnums ) {
76     @include_agents = qsearch({ 'table'     => 'agent',
77                                 'hashref'   => { 'disabled'=>'' },
78                                 'extra_sql' => ' AND '. $curuser->agentnums_sql,
79                              });
80   } elsif ( scalar(@agentnums) == 1 ) {
81     $agentnum = $agentnums[0];
82   } else {
83     @include_agents = qsearch({ 'table'     => 'agent',
84                                 'hashref'   => { 'disabled' => '', },
85                                 'extra_sql' => 'AND agentnum IN ('.
86                                                  join(',',@agentnums). ') '.
87                                                ' AND '. $curuser->agentnums_sql,
88                              });
89   }
90
91 }
92
93 if ( $agentnum ) {
94   my $agent = FS::agent->by_key($agentnum);
95   $title = $agent->agent." $title";
96 }
97
98 # usage query params
99 my( $beginning, $ending ) = FS::UI::Web::parse_beginning_ending($cgi);
100
101 if ( $beginning ) {
102   $title .= time2str('%h %o %Y ', $beginning);
103 }
104 $title .= 'through ';
105 if ( $ending == 4294967295 ) {
106   $title .= 'now';
107 } else {
108   $title .= time2str('%h %o %Y', $ending);
109 }
110
111 # can also show a specific customer / service. the main query will handle
112 # agent restrictions, but we need a list of the services to ask the export
113 # for usage data.
114 my $cust_main;
115 if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
116   $cust_main = qsearchs( {
117     'table'     => 'cust_main',
118     'hashref'   => { 'custnum' => $1 },
119     'extra_sql' => ' AND '. $curuser->agentnums_sql,
120   });
121   die "Customer not found!" unless $cust_main;
122   # then only report on this agent
123   $agentnum = $cust_main->agentnum;
124   @include_agents = ();
125   # and announce that we're doing it
126   $title .= ' - ' . $cust_main->name_short;
127 }
128
129 my @exports;
130 if ( $opt{exports} ) {
131   @exports = @{ $opt{exports} };
132 } elsif ( $cgi->param('exportnum') ) {
133   foreach my $exportnum ($cgi->param('exportnum')) {
134     $exportnum =~ /^(\d+)$/
135       or die "illegal export: '".$cgi->param('exportnum')."'";
136     my $export = FS::part_export->by_key($1)
137       or die "exportnum $1 not found";
138     $export->exporttype =~ /sqlradius/
139       or die "exportnum ".$export->exportnum." is type ".$export->exporttype.
140              ", not sqlradius";
141     push @exports, $export;
142   }
143   die "exportnum required" unless @exports;
144 } else {
145   # do something sensible if no exports were selected
146   @exports = FS::part_export::sqlradius->all_sqlradius;
147 }
148
149 my %usage_param = (
150   stoptime_start  => $beginning,
151   stoptime_end    => $ending,
152   summarize       => 1
153 );
154
155 my @total_usage = ('', 0, 0, 0); # username, input, output, input + output
156
157 # a single sub to collect data for each package, aggregated across both
158 # services and exports.  when we add per-service breakdown, this should also
159 # keep the per-service data, but not needed yet
160 my $cust_pkg_stats_sub = sub {
161   my $cust_pkg = shift;
162   if (! $cust_pkg->get('_stats') ) {
163     my ($upbytes, $downbytes, $totalbytes) = (0, 0, 0);
164     my $display_username;
165     foreach my $svcnum ( split(',', $cust_pkg->get('svcnums_concat')) ) {
166       foreach my $export (@exports) {
167         my $svc = FS::cust_svc->by_key($svcnum)->svc_x;
168         my $username = $export->export_username($svc);
169         my $usage = $export->usage_sessions({ %usage_param, 'svc' => $svc });
170         # returns arrayref with one row
171         $upbytes += $usage->[0]->{'acctinputoctets'};
172         $downbytes += $usage->[0]->{'acctoutputoctets'};
173         # in combined services mode with multiple users/MAC addresses per
174         # package, this will just show one of them arbitrarily.
175         $display_username ||= $username;
176       }
177     }
178     $total_usage[1] += $upbytes;
179     $total_usage[2] += $downbytes;
180     $total_usage[3] += $upbytes + $downbytes;
181     $cust_pkg->set('_stats', [ $display_username,
182                                $upbytes,
183                                $downbytes,
184                                $upbytes + $downbytes ]);
185   }
186   return $cust_pkg->get('_stats');
187 };
188
189 my @pkg_usage;
190 $pkg_usage[0] = sub { # username
191  return &{ $cust_pkg_stats_sub }(shift)->[0];
192 };
193 foreach my $i (1, 2, 3) { # numeric fields
194   $pkg_usage[$i] = sub { # cust_pkg arg
195     my $value = &{ $cust_pkg_stats_sub }(shift)->[$i];
196     # for now, always show in GB, rounded to 3 digits
197     $value ? bytes_to_gb($value) : '';
198   };
199 }
200
201 my $link_cust = [ $p.'view/cust_main.cgi?', 'custnum' ];
202
203 # cust_pkg search params
204 my %search_hash = ( 'agentnum' => $agentnum );
205 if ($cust_main) {
206   $search_hash{'custnum'} = $cust_main->custnum;
207 }
208
209 # construct a subquery for services/packages with relevant exports
210
211 my $group_by = ' GROUP BY pkgnum';
212 if ( !$combine_svcs ) {
213   $group_by .= ', svcnum';
214 }
215
216 my $exportnums = join(',', map { $_->get('exportnum') } @exports);
217 my $svcnums_table = 'SELECT pkgnum, ' .  FS::Record::group_concat_sql('DISTINCT svcnum', ',') . ' AS svcnums_concat
218 FROM cust_svc
219   JOIN part_svc USING (svcpart)
220   JOIN export_svc USING (svcpart)
221 WHERE exportnum IN(' . $exportnums . ')' . $group_by;
222
223 my $sql_query = FS::cust_pkg->search( \%search_hash );
224 # also get the svcnum-list column
225 $sql_query->{'select'}    .= ', svcnums_concat' .
226 # and a workaround for the implicit DISTINCTing that happens in qsearch
227                              ', NULL AS pkgnum, pkgnum AS real_pkgnum';
228 $sql_query->{'addl_from'} .= " JOIN ($svcnums_table) AS svcnums
229 USING (pkgnum)";
230 $sql_query->{'order_by'}  = ' ORDER BY cust_pkg.pkgnum, svcnums_concat'; # for stability
231
232 my $count_query = "SELECT COUNT(*) FROM cust_pkg ".
233   $sql_query->{addl_from} .
234   $sql_query->{extra_sql};
235
236 my $num_rows = FS::Record->scalar_sql($count_query);
237 my $itemname = $combine_svcs ? 'package' : 'service';
238 my @footer = (
239   '',
240   emt("[quant,_1,$itemname]", $num_rows), 
241   '', #pkg label
242   '', #username
243   map {
244     my $i = $_;
245     sub { # defer this until the rows have been processed
246       bytes_to_gb($total_usage[$i])
247     }
248   } (1,2,3)
249 );
250
251 sub bytes_to_gb {
252   $_[0] ?  sprintf('%.3f', $_[0] / (1024*1024*1024.0)) : '';
253 }
254
255 my $conf = new FS::Conf;
256 my $order_by_sql = {
257   'name'            => "CASE WHEN cust_main.company IS NOT NULL
258                                   AND cust_main.company != ''
259                              THEN CONCAT(cust_main.company,' (',cust_main.last,', ',cust_main.first,')')
260                              ELSE CONCAT(cust_main.last,', ',cust_main.first)
261                         END",
262   'display_custnum' => $conf->exists('cust_main-default_agent_custid')
263                        ? "CASE WHEN cust_main.agent_custid IS NOT NULL
264                                     AND cust_main.agent_custid != ''
265                                     AND cust_main.agent_custid ". regexp_sql. " '^[0-9]+\$'
266                                THEN CAST(cust_main.agent_custid AS BIGINT)
267                                ELSE cust_main.custnum
268                           END"
269                        : "custnum",
270 };
271
272 my $link_pkg = sub {
273   my $self = shift;
274   [ "${p}view/cust_pkg.cgi?", 'real_pkgnum' ];
275 };
276
277 my $link_svc = sub {
278   my $self = shift;
279   if ($self->svcnums_concat =~ /^(\d+)$/) {
280     return [ $p.'view/cust_svc.cgi?' . $1 ];
281   } else {
282     return '';
283   }
284 };
285 </%init>