RT#39638: VoIP Usage cost reporting [bug fixes/cleanup]
[freeside.git] / FS / FS / cdr.pm
index c2be4f2..5a1d8ea 100644 (file)
@@ -214,6 +214,7 @@ sub table_info {
         #'upstream_rateplanid'   => '',
         #'ratedetailnum'         => '',
         'rated_price'           => 'Rated price',
         #'upstream_rateplanid'   => '',
         #'ratedetailnum'         => '',
         'rated_price'           => 'Rated price',
+        'rated_cost'            => 'Rated cost',
         #'distance'              => '',
         #'islocal'               => '',
         #'calltypenum'           => '',
         #'distance'              => '',
         #'islocal'               => '',
         #'calltypenum'           => '',
@@ -338,7 +339,7 @@ sub check {
   #check the foreign keys even?
   #do we want to outright *reject* the CDR?
   my $error =
   #check the foreign keys even?
   #do we want to outright *reject* the CDR?
   my $error =
-       $self->ut_numbern('acctid')
+       $self->ut_numbern('acctid');
 
   #add a config option to turn these back on if someone needs 'em
   #
 
   #add a config option to turn these back on if someone needs 'em
   #
@@ -350,7 +351,7 @@ sub check {
   #
   #  # Telstra =1, Optus = 2, RSL COM = 3
   #  || $self->ut_foreign_keyn('carrierid', 'cdr_carrier', 'carrierid' )
   #
   #  # Telstra =1, Optus = 2, RSL COM = 3
   #  || $self->ut_foreign_keyn('carrierid', 'cdr_carrier', 'carrierid' )
-  ;
+
   return $error if $error;
 
   $self->SUPER::check;
   return $error if $error;
 
   $self->SUPER::check;
@@ -368,7 +369,14 @@ to inspect other field.
 sub is_tollfree {
   my $self = shift;
   my $field = scalar(@_) ? shift : 'dst';
 sub is_tollfree {
   my $self = shift;
   my $field = scalar(@_) ? shift : 'dst';
-  ( $self->$field() =~ /^(\+?1)?8(8|([02-7])\3)/ ) ? 1 : 0;
+  my $country = $conf->config('tollfree-country') || '';
+  if ( $country eq 'AU' ) { 
+    ( $self->$field() =~ /^(\+?61)?(1800|1300)/ ) ? 1 : 0;
+  } elsif ( $country eq 'NZ' ) { 
+    ( $self->$field() =~ /^(\+?64)?(800|508)/ ) ? 1 : 0;
+  } else { #NANPA (US/Canaada)
+    ( $self->$field() =~ /^(\+?1)?8(8|([02-7])\3)/ ) ? 1 : 0;
+  }
 }
 
 =item set_charged_party
 }
 
 =item set_charged_party
@@ -456,7 +464,9 @@ Sets the status and rated price.
 
 Available options are: inbound, rated_pretty_dst, rated_regionname,
 rated_seconds, rated_minutes, rated_granularity, rated_ratedetailnum,
 
 Available options are: inbound, rated_pretty_dst, rated_regionname,
 rated_seconds, rated_minutes, rated_granularity, rated_ratedetailnum,
-rated_classnum, rated_ratename.
+rated_classnum, rated_ratename.  If rated_ratedetailnum is provided,
+will also set a recalculated L</rate_cost> in the rated_cost field 
+after the other fields are set (does not work with inbound.)
 
 If there is an error, returns the error, otherwise returns false.
 
 
 If there is an error, returns the error, otherwise returns false.
 
@@ -494,6 +504,8 @@ sub set_status_and_rated_price {
         qw( pretty_dst regionname seconds minutes granularity
             ratedetailnum classnum ratename );
     $self->svcnum($svcnum) if $svcnum;
         qw( pretty_dst regionname seconds minutes granularity
             ratedetailnum classnum ratename );
     $self->svcnum($svcnum) if $svcnum;
+    $self->rated_cost($self->rate_cost) if $opt{'rated_ratedetailnum'};
+
     return $self->replace();
 
   }
     return $self->replace();
 
   }
@@ -792,8 +804,8 @@ sub rate_prefix {
 
   }
 
 
   }
 
+  my $regionnum = $rate_detail->dest_regionnum;
   my $rate_region = $rate_detail->dest_region;
   my $rate_region = $rate_detail->dest_region;
-  my $regionnum = $rate_region->regionnum;
   warn "  found rate for regionnum $regionnum ".
        "and rate detail $rate_detail\n"
     if $DEBUG;
   warn "  found rate for regionnum $regionnum ".
        "and rate detail $rate_detail\n"
     if $DEBUG;
@@ -820,9 +832,10 @@ sub rate_prefix {
   # We don't round _anything_ (except granularizing) 
   # until the final $charge = sprintf("%.2f"...).
 
   # We don't round _anything_ (except granularizing) 
   # until the final $charge = sprintf("%.2f"...).
 
-  my $seconds_left = $part_pkg->option_cacheable('use_duration')
-                       ? $self->duration
-                       : $self->billsec;
+  my $rated_seconds = $part_pkg->option_cacheable('use_duration')
+                        ? $self->duration
+                        : $self->billsec;
+  my $seconds_left = $rated_seconds;
 
   #no, do this later so it respects (group) included minutes
   #  # charge for the first (conn_sec) seconds
 
   #no, do this later so it respects (group) included minutes
   #  # charge for the first (conn_sec) seconds
@@ -830,10 +843,15 @@ sub rate_prefix {
   #  $seconds_left -= $seconds; 
   #  $weektime     += $seconds;
   #  my $charge = $rate_detail->conn_charge; 
   #  $seconds_left -= $seconds; 
   #  $weektime     += $seconds;
   #  my $charge = $rate_detail->conn_charge; 
-  my $seconds = 0;
+  #my $seconds = 0;
   my $charge = 0;
   my $connection_charged = 0;
 
   my $charge = 0;
   my $connection_charged = 0;
 
+  # before doing anything else, if there's an upstream multiplier and 
+  # an upstream price, add that to the charge. (usually the rate detail 
+  # will then have a minute charge of zero, but not necessarily.)
+  $charge += ($self->upstream_price || 0) * $rate_detail->upstream_mult_charge;
+
   my $etime;
   while($seconds_left) {
     my $ratetimenum = $rate_detail->ratetimenum; # may be empty
   my $etime;
   while($seconds_left) {
     my $ratetimenum = $rate_detail->ratetimenum; # may be empty
@@ -888,7 +906,7 @@ sub rate_prefix {
       $seconds_left = 0;
     }
 
       $seconds_left = 0;
     }
 
-    $seconds += $charge_sec;
+    #$seconds += $charge_sec;
 
     if ( $rate_detail->min_included ) {
       # the old, kind of deprecated way to do this:
 
     if ( $rate_detail->min_included ) {
       # the old, kind of deprecated way to do this:
@@ -981,8 +999,8 @@ sub rate_prefix {
     $price,
     $opt{'svcnum'},
     'rated_pretty_dst'    => $pretty_dst,
     $price,
     $opt{'svcnum'},
     'rated_pretty_dst'    => $pretty_dst,
-    'rated_regionname'    => $rate_region->regionname,
-    'rated_seconds'       => $seconds,
+    'rated_regionname'    => ($rate_region ? $rate_region->regionname : ''),
+    'rated_seconds'       => $rated_seconds, #$seconds,
     'rated_granularity'   => $rate_detail->sec_granularity, #$granularity
     'rated_ratedetailnum' => $rate_detail->ratedetailnum,
     'rated_classnum'      => $rate_detail->classnum, #rated_ratedetailnum?
     'rated_granularity'   => $rate_detail->sec_granularity, #$granularity
     'rated_ratedetailnum' => $rate_detail->ratedetailnum,
     'rated_classnum'      => $rate_detail->classnum, #rated_ratedetailnum?
@@ -1049,6 +1067,36 @@ sub rate_single_price {
 
 }
 
 
 }
 
+=item rate_cost
+
+Rates an already-rated CDR according to the cost fields from the rate plan.
+
+Returns the amount.
+
+=cut
+
+sub rate_cost {
+  my $self = shift;
+
+  return 0 unless $self->rated_ratedetailnum;
+
+  my $rate_detail =
+    qsearchs('rate_detail', { 'ratedetailnum' => $self->rated_ratedetailnum } );
+
+  my $charge = 0;
+  $charge += ($self->upstream_price || 0) * ($rate_detail->upstream_mult_cost);
+
+  if ( $self->rated_granularity == 0 ) {
+    $charge += $rate_detail->min_cost;
+  } else {
+    my $minutes = $self->rated_seconds / 60;
+    $charge += $rate_detail->conn_cost + $minutes * $rate_detail->min_cost;
+  }
+
+  sprintf('%.2f', $charge + .00001 );
+
+}
+
 =item cdr_termination [ TERMPART ]
 
 =cut
 =item cdr_termination [ TERMPART ]
 
 =cut
@@ -1210,6 +1258,10 @@ my %export_names = (
     'name'           => 'Summary, one line per destination prefix',
     'invoice_header' => 'Caller,Rate,Calls,Minutes,Price',
   },
     'name'           => 'Summary, one line per destination prefix',
     'invoice_header' => 'Caller,Rate,Calls,Minutes,Price',
   },
+  'sum_count_class' => {
+    'name'           => 'Summary, one line per usage class',
+    'invoice_header' => 'Caller,Class,Calls,Price',
+  },
 );
 
 my %export_formats = ();
 );
 
 my %export_formats = ();
@@ -1221,7 +1273,7 @@ sub export_formats {
   my $conf = new FS::Conf;
   my $date_format = $conf->config('date_format') || '%m/%d/%Y';
 
   my $conf = new FS::Conf;
   my $date_format = $conf->config('date_format') || '%m/%d/%Y';
 
-  # call duration in the largest units that accurately reflect the  granularity
+  # call duration in the largest units that accurately reflect the granularity
   my $duration_sub = sub {
     my($cdr, %opt) = @_;
     my $sec = $opt{seconds} || $cdr->billsec;
   my $duration_sub = sub {
     my($cdr, %opt) = @_;
     my $sec = $opt{seconds} || $cdr->billsec;
@@ -1411,7 +1463,7 @@ as keys (for use with part_pkg::voip_cdr) and "pretty" format names as values.
 sub invoice_formats {
   map { ($_ => $export_names{$_}->{'name'}) }
     grep { $export_names{$_}->{'invoice_header'} }
 sub invoice_formats {
   map { ($_ => $export_names{$_}->{'name'}) }
     grep { $export_names{$_}->{'invoice_header'} }
-    keys %export_names;
+    sort keys %export_names;
 }
 
 =item invoice_header FORMAT
 }
 
 =item invoice_header FORMAT
@@ -1490,8 +1542,8 @@ as keys (for use with batch_import) and "pretty" format names as values.
 
 my %cdr_info;
 foreach my $INC ( @INC ) {
 
 my %cdr_info;
 foreach my $INC ( @INC ) {
-  warn "globbing $INC/FS/cdr/*.pm\n" if $DEBUG;
-  foreach my $file ( glob("$INC/FS/cdr/*.pm") ) {
+  warn "globbing $INC/FS/cdr/[a-z]*.pm\n" if $DEBUG;
+  foreach my $file ( glob("$INC/FS/cdr/[a-z]*.pm") ) {
     warn "attempting to load CDR format info from $file\n" if $DEBUG;
     $file =~ /\/(\w+)\.pm$/ or do {
       warn "unrecognized file in $INC/FS/cdr/: $file\n";
     warn "attempting to load CDR format info from $file\n" if $DEBUG;
     $file =~ /\/(\w+)\.pm$/ or do {
       warn "unrecognized file in $INC/FS/cdr/: $file\n";
@@ -1580,7 +1632,7 @@ sub _cdr_date_parse {
     # optionally without seconds
     ($mon, $day, $year, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
     $sec = 0 if !defined($sec);
     # optionally without seconds
     ($mon, $day, $year, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
     $sec = 0 if !defined($sec);
-  } elsif ( $date  =~ /^\s*(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d+\.\d+)(\D|$)/ ) {
+   } elsif ( $date  =~ /^\s*(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\.\d+)$/ ) {
     # broadsoft: 20081223201938.314
     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
   } elsif ( $date  =~ /^\s*(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\d+(\D|$)/ ) {
     # broadsoft: 20081223201938.314
     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
   } elsif ( $date  =~ /^\s*(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\d+(\D|$)/ ) {
@@ -1590,7 +1642,7 @@ sub _cdr_date_parse {
     # WIP: 20100329121420
     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
   } elsif ( $date =~ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/) {
     # WIP: 20100329121420
     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
   } elsif ( $date =~ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/) {
-    # Telos
+    # Telos 2014-10-10T05:30:33Z
     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
     $options{gmt} = 1;
   } else {
     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
     $options{gmt} = 1;
   } else {