[freeside-commits] branch FREESIDE_2_3_BRANCH updated. d5c51ff8d85f1f9ad64961878dcba6adb7968523

Mark Wells mark at 420.am
Tue Mar 5 16:51:00 PST 2013


The branch, FREESIDE_2_3_BRANCH has been updated
       via  d5c51ff8d85f1f9ad64961878dcba6adb7968523 (commit)
      from  ede90e4c0ff771538dfe184c10d8fb1a3f2bb469 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit d5c51ff8d85f1f9ad64961878dcba6adb7968523
Author: Mark Wells <mark at freeside.biz>
Date:   Tue Mar 5 16:47:48 2013 -0800

    display inbound CDRs in selfservice, #18316; Taqua caller ID, #18574

diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
index f949246..be87e59 100644
--- a/FS/FS/ClientAPI/MyAccount.pm
+++ b/FS/FS/ClientAPI/MyAccount.pm
@@ -1692,7 +1692,34 @@ sub list_svcs {
               } else {
                 $hash{'name'} = $cust_main->name;
               }
+            } elsif ( $svcdb eq 'svc_phone' ) {
+              # could potentially show lots of things...
+              $hash{'outbound'} = 1;
+              $hash{'inbound'}  = 0;
+              if ( $part_pkg->plan eq 'voip_inbound' ) {
+                $hash{'outbound'} = 0;
+                $hash{'inbound'}  = 1;
+              } elsif ( $part_pkg->option('selfservice_inbound_format')
+                    or  $conf->config('selfservice-default_inbound_cdr_format')
+              ) {
+                $hash{'inbound'}  = 1;
+              }
+              foreach (qw(inbound outbound)) {
+                # hmm...we can't filter by status here, because there might
+                # not be cdr_terminations at all.  have to go by date.
+                # find all since the last bill date.
+                # XXX cdr types?  we are going to need them.
+                if ( $hash{$_} ) {
+                  my $sum_cdr = $svc_x->sum_cdrs(
+                    'inbound' => ( $_ eq 'inbound' ? 1 : 0 ),
+                    'begin'   => ($cust_pkg->last_bill || 0),
+                    'nonzero' => 1,
+                  );
+                  $hash{$_} = $sum_cdr->hashref;
+                }
+              }
             }
+
             # elsif ( $svcdb eq 'svc_phone' || $svcdb eq 'svc_port' ) {
             #  %hash = (
             #    %hash,
@@ -1963,7 +1990,7 @@ sub _list_cdr_usage {
   # XXX CDR type support...
   my($svc_phone, $begin, $end, %opt) = @_;
   map [ $_->downstream_csv(%opt, 'keeparray' => 1) ],
-    $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, );
+    $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, %opt );
 }
 
 sub list_cdr_usage {
@@ -1993,18 +2020,21 @@ sub _usage_details {
   my %callback_opt;
   my $header = [];
   if ( $svcdb eq 'svc_phone' ) {
-    my $format   = $cust_pkg->part_pkg->option('output_format') || '';
-    $format = '' if $format =~ /^sum_/;
-    # sensible default if there is no format or it's a summary format
-    if ( $cust_pkg->part_pkg->plan eq 'voip_inbound' ) {
-      $format ||= 'source_default';
+    my $conf = FS::Conf->new;
+    my $format = '';
+    if ( $p->{inbound} ) {
+      $format = $cust_pkg->part_pkg->option('selfservice_inbound_format') 
+                || $conf->config('selfservice-default_inbound_cdr_format')
+                || 'source_default';
       $callback_opt{inbound} = 1;
+    } else {
+      $format = $cust_pkg->part_pkg->option('selfservice_format')
+                || $conf->config('selfservice-default_cdr_format')
+                || 'default';
     }
-    else {
-      $format ||= 'default';
-    }
-    
+
     $callback_opt{format} = $format;
+    $callback_opt{use_clid} = 1;
     $header = [ split(',', FS::cdr::invoice_header($format) ) ];
   }
 
@@ -2063,6 +2093,7 @@ sub _usage_details {
     'svcnum'    => $p->{svcnum},
     'beginning' => $p->{beginning},
     'ending'    => $p->{ending},
+    'inbound'   => $p->{inbound},
     'previous'  => ($previous > $start) ? $previous : $start,
     'next'      => ($next < $end) ? $next : $end,
     'header'    => $header,
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index c0c7c03..2b9f7b9 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -710,6 +710,18 @@ my %payment_gateway_options = (
   },
 );
 
+my @cdr_formats = (
+  '' => '',
+  'default' => 'Default',
+  'source_default' => 'Default with source',
+  'accountcode_default' => 'Default plus accountcode',
+  'description_default' => 'Default with description field as destination',
+  'basic' => 'Basic',
+  'simple' => 'Simple',
+  'simple2' => 'Simple with source',
+  'accountcode_simple' => 'Simple with accountcode',
+);
+
 #Billing (81 items)
 #Invoicing (50 items)
 #UI (69 items)
@@ -4628,6 +4640,13 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'cdr-taqua-callerid_rewrite',
+    'section'     => 'telephony',
+    'description' => 'For the Taqua CDR format, pull Caller ID blocking information from secondary CDRs.',
+    'type'        => 'checkbox',
+  },
+
+  {
     'key'         => 'cust_pkg-show_autosuspend',
     'section'     => 'UI',
     'description' => 'Show package auto-suspend dates.  Use with caution for now; can slow down customer view for large insallations.',
@@ -5199,6 +5218,22 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'selfservice-default_cdr_format',
+    'section'     => 'self-service',
+    'description' => 'Format for showing outbound CDRs in self-service.  The per-package option overrides this.',
+    'type'        => 'select',
+    'select_hash' => \@cdr_formats,
+  },
+
+  {
+    'key'         => 'selfservice-default_inbound_cdr_format',
+    'section'     => 'self-service',
+    'description' => 'Format for showing inbound CDRs in self-service.  The per-package option overrides this.  Leave blank to avoid showing these CDRs.',
+    'type'        => 'select',
+    'select_hash' => \@cdr_formats,
+  },
+
+  {
     'key'         => 'logout-timeout',
     'section'     => 'UI',
     'description' => 'If set, automatically log users out of the backoffice after this many minutes.',
diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm
index 3e89f55..bf7a907 100644
--- a/FS/FS/cdr.pm
+++ b/FS/FS/cdr.pm
@@ -630,6 +630,8 @@ sub export_formats {
     length($price) ? ($opt{money_char} . $price) : '';
   };
 
+  my $src_sub = sub { $_[0]->clid || $_[0]->src };
+
   %export_formats = (
     'simple' => [
       sub { time2str($date_format, shift->calldate_unix ) },   #DATE
@@ -644,7 +646,7 @@ sub export_formats {
       sub { time2str($date_format, shift->calldate_unix ) },   #DATE
       sub { time2str('%r', shift->calldate_unix ) },   #TIME
       #'userfield',                                     #USER
-      'src',                                           #called from
+      $src_sub,                                           #called from
       'dst',                                           #NUMBER_DIALED
       $duration_sub,                                   #DURATION
       #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
@@ -653,7 +655,7 @@ sub export_formats {
     'accountcode_simple' => [
       sub { time2str($date_format, shift->calldate_unix ) },   #DATE
       sub { time2str('%r', shift->calldate_unix ) },   #TIME
-      'src',                                           #called from
+      $src_sub,                                           #called from
       'accountcode',                                   #NUMBER_DIALED
       $duration_sub,                                   #DURATION
       $price_sub,
@@ -661,14 +663,14 @@ sub export_formats {
     'sum_duration' => [ 
       # for summary formats, the CDR is a fictitious object containing the 
       # total billsec and the phone number of the service
-      'src',
+      $src_sub,
       sub { my($cdr, %opt) = @_; $opt{ratename} },
       sub { my($cdr, %opt) = @_; $opt{count} },
       sub { my($cdr, %opt) = @_; int($opt{seconds}/60).'m' },
       $price_sub,
     ],
     'sum_count' => [
-      'src',
+      $src_sub,
       sub { my($cdr, %opt) = @_; $opt{ratename} },
       sub { my($cdr, %opt) = @_; $opt{count} },
       $price_sub,
@@ -702,7 +704,7 @@ sub export_formats {
       $price_sub,
     ],
   );
-  $export_formats{'source_default'} = [ 'src', @{ $export_formats{'default'} }, ];
+  $export_formats{'source_default'} = [ $src_sub, @{ $export_formats{'default'} }, ];
   $export_formats{'accountcode_default'} =
     [ @{ $export_formats{'default'} }[0,1],
       'accountcode',
@@ -710,7 +712,7 @@ sub export_formats {
     ];
   my @default = @{ $export_formats{'default'} };
   $export_formats{'description_default'} = 
-    [ 'src', @default[0..2], 
+    [ $src_sub, @default[0..2], 
       sub { my($cdr, %opt) = @_; $cdr->description },
       @default[4,5] ];
 
diff --git a/FS/FS/cdr/taqua62.pm b/FS/FS/cdr/taqua62.pm
index 862018e..aa94630 100644
--- a/FS/FS/cdr/taqua62.pm
+++ b/FS/FS/cdr/taqua62.pm
@@ -20,7 +20,9 @@ use FS::cdr qw(_cdr_date_parser_maker);
       my($cdr, $field, $conf, $hashref) = @_;
       $hashref->{skiprow} = 1
         unless ($field == 0 && $cdr->disposition == 100       )  #regular CDR
-            || ($field == 1 && $cdr->lastapp     eq 'acctcode'); #accountcode
+            || ($field == 1 && $cdr->lastapp     eq 'acctcode')  #accountcode
+            || ($field == 1 && $cdr->lastapp     eq 'CallerId')  #CID blocking
+            ;
       $cdr->cdrtypenum($field);
     },
 
diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm
index 73b0aed..9943cfc 100644
--- a/FS/FS/part_pkg/voip_cdr.pm
+++ b/FS/FS/part_pkg/voip_cdr.pm
@@ -49,6 +49,11 @@ tie my %unrateable_opts, 'Tie::IxHash',
   2  => 'Flag for later review',
 ;
 
+tie my %detail_formats, 'Tie::IxHash',
+  '' => '',
+  FS::cdr::invoice_formats()
+;
+
 %info = (
   'name' => 'VoIP rating by plan of CDR records in an internal (or external) SQL table',
   'shortname' => 'VoIP/telco CDR rating (standard)',
@@ -206,12 +211,25 @@ tie my %unrateable_opts, 'Tie::IxHash',
                       },
 
     #false laziness w/cdr_termination.pm
-    'output_format' => { 'name' => 'CDR invoice display format',
+    'output_format' => { 'name' => 'CDR display format for invoices',
                          'type' => 'select',
-                         'select_options' => { FS::cdr::invoice_formats() },
+                         'select_options' => \%detail_formats,
                          'default'        => 'default', #XXX test
                        },
 
+    'selfservice_format' => 
+      { 'name' => 'CDR display format for selfservice',
+        'type' => 'select',
+        'select_options' => \%detail_formats,
+        'default' => 'default'
+      },
+    'selfservice_inbound_format' =>
+      { 'name' => 'Inbound CDR display format for selfservice',
+        'type' => 'select',
+        'select_options' => \%detail_formats,
+        'default' => ''
+      },
+
     'usage_section' => { 'name' => 'Section in which to place usage charges (whether separated or not): ',
                        },
 
@@ -287,7 +305,9 @@ tie my %unrateable_opts, 'Tie::IxHash',
                        skip_max_callers
                        use_duration
                        411_rewrite
-                       output_format usage_mandate summarize_usage usage_section
+                       output_format 
+                       selfservice_format selfservice_inbound_format
+                       usage_mandate summarize_usage usage_section
                        bill_every_call bill_inactive_svcs
                        count_available_phones suspend_bill 
                      )
diff --git a/FS/FS/svc_phone.pm b/FS/FS/svc_phone.pm
index 118748e..097310b 100644
--- a/FS/FS/svc_phone.pm
+++ b/FS/FS/svc_phone.pm
@@ -673,13 +673,15 @@ with the chosen prefix.
 
 =item disable_src => 1: Only match on "charged_party", not "src".
 
+=item nonzero: Only return CDRs where duration > 0.
+
 =item by_svcnum: not supported for svc_phone
 
 =back
 
 =cut
 
-sub get_cdrs {
+sub _search_cdrs { # transitional; replaced with psearch_cdrs in 3.0
   my($self, %options) = @_;
   my @fields;
   my %hash;
@@ -735,19 +737,48 @@ sub get_cdrs {
   if ( $options{'end'} ) {
     push @where, 'startdate < '.  $options{'end'};
   }
+  if ( $options{'nonzero'} ) {
+    push @where, 'duration > 0';
+  }
 
   my $extra_sql = ( keys(%hash) ? ' AND ' : ' WHERE ' ). join(' AND ', @where );
 
-  my @cdrs =
-    qsearch( {
+
+  return {
       'table'      => 'cdr',
       'hashref'    => \%hash,
       'extra_sql'  => $extra_sql,
       'order_by'   => $options{'billsec_sum'} ? '' : "ORDER BY startdate $for_update",
       'select'     => $options{'billsec_sum'} ? 'sum(billsec) as billsec_sum' : '*',
-    } );
+  };
+}
 
-  @cdrs;
+sub get_cdrs {
+  qsearch(_search_cdrs(@_));
+}
+
+=item sum_cdrs
+
+Takes the same options as psearch_cdrs, but returns a single row containing
+"count" (the number of CDRs) and the sums of the following fields: duration,
+billsec, rated_price, rated_seconds, rated_minutes.
+
+Note that if any calls are not rated, their rated_* fields will be null.
+If you want to use those fields, pass the 'status' option to limit to 
+calls that have been rated.  This is intentional; please don't "fix" it.
+
+=cut
+
+sub sum_cdrs {
+  my $self = shift;
+  my $search = $self->_search_cdrs(@_);
+  $search->{'select'} = join(',',
+    'COUNT(*) AS count',
+    map { "SUM($_) AS $_" }
+      qw(duration billsec rated_price rated_seconds rated_minutes)
+  );
+  $search->{'order_by'} = '';
+  qsearchs ( $search );
 }
 
 =back
diff --git a/FS/bin/freeside-cdrrewrited b/FS/bin/freeside-cdrrewrited
index 846b0b9..482e849 100644
--- a/FS/bin/freeside-cdrrewrited
+++ b/FS/bin/freeside-cdrrewrited
@@ -30,9 +30,9 @@ die "not running; cdr-asterisk_forward_rewrite, cdr-charged_party_rewrite ".
 
 #--
 
-my %accountcode_unmatch = ();
-my $accountcode_retry = 4 * 60 * 60; # 4 hours
-my $accountcode_giveup = 4 * 24 * 60 * 60; # 4 days
+my %sessionnum_unmatch = ();
+my $sessionnum_retry = 4 * 60 * 60; # 4 hours
+my $sessionnum_giveup = 4 * 24 * 60 * 60; # 4 days
 
 while (1) {
 
@@ -42,8 +42,8 @@ while (1) {
   # instead of just doing this search like normal CDRs
 
   #hmm :/
-  my @recent = grep { ($accountcode_unmatch{$_} + $accountcode_retry) > time }
-                 keys %accountcode_unmatch;
+  my @recent = grep { ($sessionnum_unmatch{$_} + $sessionnum_retry) > time }
+                 keys %sessionnum_unmatch;
   my $extra_sql = scalar(@recent)
                     ? ' AND acctid NOT IN ('. join(',', @recent). ') '
                     : '';
@@ -92,45 +92,62 @@ while (1) {
 
     }
 
-    if ( $conf->exists('cdr-taqua-accountcode_rewrite')
-         && $cdr->lastapp eq 'acctcode' && $cdr->cdrtypenum == 1
+    if (     $cdr->cdrtypenum == 1
+         and $cdr->lastapp
+         and (
+            $conf->exists('cdr-taqua-accountcode_rewrite') or
+            $conf->exists('cdr-taqua-callerid_rewrite') )
        )
     {
 
       #find the matching CDR
-      my $primary = qsearchs('cdr', {
-        'sessionnum'  => $cdr->sessionnum,
-        'src'         => $cdr->subscriber,
-        #'accountcode' => '',
-      });
+      my %search = ( 'sessionnum' => $cdr->sessionnum );
+      if ( $cdr->lastapp eq 'acctcode' ) {
+        $search{'src'} = $cdr->subscriber;
+      } elsif ( $cdr->lastapp eq 'CallerId' ) {
+        $search{'dst'} = $cdr->subscriber;
+      }
+      my $primary = qsearchs('cdr', \%search);
 
       unless ( $primary ) {
 
         my $cantfind = "can't find primary CDR with session ". $cdr->sessionnum.
                        ", src ". $cdr->subscriber;
-        if ( $cdr->calldate_unix + $accountcode_giveup < time ) {
+        if ( $cdr->calldate_unix + $sessionnum_giveup < time ) {
           warn "ERROR: $cantfind; giving up\n";
-          push @status, 'taqua-accountcode-NOTFOUND';
+          push @status, 'taqua-sessionnum-NOTFOUND';
           $cdr->status('done'); #so it doesn't try to rate
-          delete $accountcode_unmatch{$cdr->acctid}; #so it doesn't suck mem
+          delete $sessionnum_unmatch{$cdr->acctid}; #so it doesn't suck mem
         } else {
           warn "WARNING: $cantfind; will keep trying\n";
-          $accountcode_unmatch{$cdr->acctid} = time;
+          $sessionnum_unmatch{$cdr->acctid} = time;
           next;
         }
 
       } else {
 
-        $primary->accountcode( $cdr->lastdata );
+        if ( $cdr->lastapp eq 'acctcode' ) {
+          # lastdata contains the dialed account code
+          $primary->accountcode( $cdr->lastdata );
+          push @status, 'taqua-accountcode';
+        } elsif ( $cdr->lastapp eq 'CallerId' ) {
+          # lastdata contains "allowed" or "restricted"
+          # or case variants thereof
+          if ( lc($cdr->lastdata) eq 'restricted' ) {
+            $primary->clid( 'PRIVATE' );
+          }
+          push @status, 'taqua-callerid';
+        } else {
+          warn "unknown Taqua service name: ".$cdr->lastapp."\n";
+        }
         #$primary->freesiderewritestatus( 'taqua-accountcode-primary' );
-        my $error = $primary->replace;
+        my $error = $primary->replace if $primary->modified;
         if ( $error ) {
           warn "WARNING: error rewriting primary CDR (will retry): $error\n";
           next;
         }
         $skip{$primary->acctid} = 1;
 
-        push @status, 'taqua-accountcode';
         $cdr->status('done'); #so it doesn't try to rate
 
       }
@@ -164,7 +181,10 @@ while (1) {
 sub _shouldrun {
      $conf->exists('cdr-asterisk_forward_rewrite')
   || $conf->exists('cdr-charged_party_rewrite')
-  || $conf->exists('cdr-taqua-accountcode_rewrite');
+  || $conf->exists('cdr-taqua-accountcode_rewrite')
+  || $conf->exists('cdr-taqua-callerid_rewrite')
+  || 0
+  ;
 }
 
 sub usage { 
diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
index 61361b8..efbd0cd 100644
--- a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
+++ b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
@@ -873,6 +873,7 @@ sub view_cdr_details {
     'svcnum'      => $cgi->param('svcnum'),
     'beginning'   => $cgi->param('beginning') || '',
     'ending'      => $cgi->param('ending') || '',
+    'inbound'     => $cgi->param('inbound') || 0,
   );
 }
 
diff --git a/fs_selfservice/FS-SelfService/cgi/view_cdr_details.html b/fs_selfservice/FS-SelfService/cgi/view_cdr_details.html
index b0205ec..0ee8e96 100644
--- a/fs_selfservice/FS-SelfService/cgi/view_cdr_details.html
+++ b/fs_selfservice/FS-SelfService/cgi/view_cdr_details.html
@@ -1,5 +1,6 @@
 <%= $url = "$selfurl?session=$session_id;action="; ''; %>
-<%= include('header', 'Call usage for '.
+<%= include('header', ($inbound ? 'Received calls' : 'Dialed calls' ) . 
+                       ' for '.
                        Date::Format::time2str('%b %o %Y', $beginning).
                        ' - '.
                        Date::Format::time2str('%b %o %Y', $ending)
diff --git a/fs_selfservice/FS-SelfService/cgi/view_usage.html b/fs_selfservice/FS-SelfService/cgi/view_usage.html
index fd5426a..f707668 100644
--- a/fs_selfservice/FS-SelfService/cgi/view_usage.html
+++ b/fs_selfservice/FS-SelfService/cgi/view_usage.html
@@ -62,11 +62,22 @@
 <%= scalar(@svc_acct) ? '</TABLE><BR><BR>' : '' %>
 
 <%= if ( @svc_phone ) {
+      %any = ();
+      for my $dir (qw(outbound inbound)) {
+        $any{$dir} = grep { $_->{$dir} } @svc_phone;
+      }
       $OUT.= '<FONT SIZE="4">Call usage</FONT><BR><BR>
               <TABLE BGCOLOR="#cccccc">
                 <TR>
-                  <TH ALIGN="left">Number</TH>'; #"Account" ?
-                                                 #what else?
+                  <TH ALIGN="left">Number</TH>';
+      if ( $any{outbound} ) {
+        $OUT .= '
+                  <TH>Dialed</TH>';
+      }
+      if ( $any{inbound} ) {
+        $OUT .= '
+                  <TH>Received</TH>';
+      }
       $OUT .= '</TR>';
     } else {
       $OUT .= '';
@@ -76,10 +87,27 @@
 <%= foreach my $svc_phone ( @svc_phone ) {
       my $link = "${url}view_cdr_details;".
         "svcnum=$svc_phone->{'svcnum'};beginning=0;ending=0";
-  $OUT .= '<TR><TD>';
-    $OUT .= qq!<A HREF="$link">!. $svc_phone->{'label'}. ': '. $svc_phone->{'value'}.'</A>';
-  $OUT .= '</TD></TR>';
+  $OUT .= '<TR><TD>'. $svc_phone->{'label'}. ': '. $svc_phone->{'value'};
+  $OUT .= '</TD>';
+  # usage summary w/ links
+  for my $dir (qw(outbound inbound)) {
+    if ( $dir eq 'inbound' ) {
+      $link .= ';inbound=1';
+    }
+    if ( $svc_phone->{$dir} ) {
+      $OUT .= '<TD ALIGN="right">'.qq!<A HREF="$link">! .
+        sprintf('%d calls (%.0f minutes)',
+          $svc_phone->{$dir}->{'count'},
+          $svc_phone->{$dir}->{'duration'} / 60
+        ) .
+        '</A></TD>';
+    } elsif ( $any{$dir} )  {
+      $OUT .= '<TD></TD>';
+    }
   }
+  $OUT .= '</TR>';
+}
+'';
 %>
 
 <%= scalar(@svc_phone) ? '</TABLE><BR><BR>' : '' %>

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/ClientAPI/MyAccount.pm                       |   51 +++++++++++++---
 FS/FS/Conf.pm                                      |   35 +++++++++++
 FS/FS/cdr.pm                                       |   14 +++--
 FS/FS/cdr/taqua62.pm                               |    4 +-
 FS/FS/part_pkg/voip_cdr.pm                         |   26 ++++++++-
 FS/FS/svc_phone.pm                                 |   41 ++++++++++++--
 FS/bin/freeside-cdrrewrited                        |   60 +++++++++++++-------
 fs_selfservice/FS-SelfService/cgi/selfservice.cgi  |    1 +
 .../FS-SelfService/cgi/view_cdr_details.html       |    3 +-
 fs_selfservice/FS-SelfService/cgi/view_usage.html  |   38 +++++++++++--
 10 files changed, 222 insertions(+), 51 deletions(-)




More information about the freeside-commits mailing list