X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcdr.pm;h=5658be94bede972a1b9cf22f05528fc94485f9b0;hb=c27f80ec10180391d00286bf50dfbf09a96c1b00;hp=fdec921ee8efa6500b77e0aa1f06b12021edca8b;hpb=0f3189c19c3cb41f7daafbc102d692a8426cb427;p=freeside.git diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm index fdec921ee..5658be94b 100644 --- a/FS/FS/cdr.pm +++ b/FS/FS/cdr.pm @@ -11,6 +11,7 @@ use Date::Parse; use Date::Format; use Time::Local; use List::Util qw( first min ); +use Text::CSV_XS; use FS::UID qw( dbh ); use FS::Conf; use FS::Record qw( qsearch qsearchs ); @@ -91,6 +92,8 @@ following fields are currently supported: =item dst_ip_addr - Destination IP address (same) +=item dst_term - Terminating destination number (if different from dst) + =item startdate - Start of call (UNIX-style integer timestamp) =item answerdate - Answer time of call (UNIX-style integer timestamp) @@ -193,6 +196,7 @@ sub table_info { #'lastdata' => '', 'src_ip_addr' => 'Source IP', 'dst_ip_addr' => 'Dest. IP', + 'dst_term' => 'Termination dest.', 'startdate' => 'Start date', 'answerdate' => 'Answer date', 'enddate' => 'End date', @@ -325,6 +329,10 @@ sub check { $self->billsec( $self->enddate - $self->answerdate ); } + if ( ! $self->enddate && $self->startdate && $self->duration ) { + $self->enddate( $self->startdate + $self->duration ); + } + $self->set_charged_party; #check the foreign keys even? @@ -421,12 +429,25 @@ sub set_charged_party { Sets the status to the provided string. If there is an error, returns the error, otherwise returns false. +If status is being changed from 'rated' to some other status, also removes +any usage allocations to this CDR. + =cut sub set_status { my($self, $status) = @_; + my $old_status = $self->freesidestatus; $self->freesidestatus($status); - $self->replace; + my $error = $self->replace; + if ( $old_status eq 'rated' and $status ne 'done' ) { + # deallocate any usage + foreach (qsearch('cdr_cust_pkg_usage', {acctid => $self->acctid})) { + my $cust_pkg_usage = $_->cust_pkg_usage; + $cust_pkg_usage->set('minutes', $cust_pkg_usage->minutes + $_->minutes); + $error ||= $cust_pkg_usage->replace || $_->delete; + } + } + $error; } =item set_status_and_rated_price STATUS RATED_PRICE [ SVCNUM [ OPTION => VALUE ... ] ] @@ -573,7 +594,7 @@ reference of the number of included minutes and will be decremented by the rated minutes of this CDR. region_group_included_minutes_hashref is required for prefix price plans which -have included minues (otehrwise unused/ignored). It should be set to an empty +have included minues (otherwise unused/ignored). It should be set to an empty hashref at the start of a month's rating and then preserved across CDRs. =cut @@ -598,6 +619,7 @@ our %interval_cache = (); # for timed rates sub rate_prefix { my( $self, %opt ) = @_; my $part_pkg = $opt{'part_pkg'} or return "No part_pkg specified"; + my $cust_pkg = $opt{'cust_pkg'}; my $da_rewrote = 0; # this will result in those CDRs being marked as done... is that @@ -625,14 +647,52 @@ sub rate_prefix { ); } + if ( $part_pkg->option_cacheable('skip_same_customer') + and ! $self->is_tollfree ) { + my ($dst_countrycode, $dst_number) = $self->parse_number( + column => 'dst', + international_prefix => $part_pkg->option_cacheable('international_prefix'), + domestic_prefix => $part_pkg->option_cacheable('domestic_prefix'), + ); + my $dst_same_cust = FS::Record->scalar_sql( + 'SELECT COUNT(svc_phone.svcnum) AS count '. + 'FROM cust_pkg ' . + 'JOIN cust_svc USING (pkgnum) ' . + 'JOIN svc_phone USING (svcnum) ' . + 'WHERE svc_phone.countrycode = ' . dbh->quote($dst_countrycode) . + ' AND svc_phone.phonenum = ' . dbh->quote($dst_number) . + ' AND cust_pkg.custnum = ' . $cust_pkg->custnum, + ); + if ( $dst_same_cust > 0 ) { + warn "not charging for CDR (same source and destination customer)\n" if $DEBUG; + return $self->set_status_and_rated_price( 'skipped', + 0, + $opt{'svcnum'}, + ); + } + } + + + ### # look up rate details based on called station id # (or calling station id for toll free calls) ### + my $eff_ratenum = $self->is_tollfree('accountcode') + ? $part_pkg->option_cacheable('accountcode_tollfree_ratenum') + : ''; + my( $to_or_from, $column ); - if ( $self->is_tollfree && ! $part_pkg->option_cacheable('disable_tollfree') ) + if( + ( $self->is_tollfree + && ! $part_pkg->option_cacheable('disable_tollfree') + ) + or ( $eff_ratenum + && $part_pkg->option_cacheable('accountcode_tollfree_field') eq 'src' + ) + ) { #tollfree call $to_or_from = 'from'; $column = 'src'; @@ -653,10 +713,6 @@ sub rate_prefix { #asterisks here causes inserting the detail to barf, so: $pretty_dst =~ s/\*//g; - my $eff_ratenum = $self->is_tollfree('accountcode') - ? $part_pkg->option_cacheable('accountcode_tollfree_ratenum') - : ''; - my $ratename = ''; my $intrastate_ratenum = $part_pkg->option_cacheable('intrastate_ratenum'); if ( $intrastate_ratenum && !$self->is_tollfree ) { @@ -823,11 +879,6 @@ sub rate_prefix { $seconds_left -= $charge_sec; - my $included_min = $opt{'region_group_included_min_hashref'} || {}; - - $included_min->{$regionnum}{$ratetimenum} = $rate_detail->min_included - unless exists $included_min->{$regionnum}{$ratetimenum}; - my $granularity = $rate_detail->sec_granularity; my $minutes; @@ -845,20 +896,40 @@ sub rate_prefix { $seconds += $charge_sec; + if ( $rate_detail->min_included ) { + # the old, kind of deprecated way to do this + my $included_min = $opt{'region_group_included_min_hashref'} || {}; + + # by default, set the included minutes for this region/time to + # what's in the rate_detail + $included_min->{$regionnum}{$ratetimenum} = $rate_detail->min_included + unless exists $included_min->{$regionnum}{$ratetimenum}; + + # the way that doesn't work + #my $region_group = ($part_pkg->option_cacheable('min_included') || 0) > 0; - my $region_group = ($part_pkg->option_cacheable('min_included') || 0) > 0; + #${$opt{region_group_included_min}} -= $minutes + # if $region_group && $rate_detail->region_group; - ${$opt{region_group_included_min}} -= $minutes - if $region_group && $rate_detail->region_group; + if ( $included_min->{$regionnum}{$ratetimenum} >= $minutes ) { + $charge_sec = 0; + $included_min->{$regionnum}{$ratetimenum} -= $minutes; + } else { + $charge_sec -= ($included_min->{$regionnum}{$ratetimenum} * 60); + $included_min->{$regionnum}{$ratetimenum} = 0; + } + } else { + # the new way! + my $applied_min = $cust_pkg->apply_usage( + 'cdr' => $self, + 'rate_detail' => $rate_detail, + 'minutes' => $minutes + ); + # for now, usage pools deal only in whole minutes + $charge_sec -= $applied_min * 60; + } - $included_min->{$regionnum}{$ratetimenum} -= $minutes; - if ( - $included_min->{$regionnum}{$ratetimenum} <= 0 - && ( ${$opt{region_group_included_min}} <= 0 - || ! $rate_detail->region_group - ) - ) - { + if ( $charge_sec > 0 ) { #NOW do connection charges here... right? #my $conn_seconds = min($seconds_left, $rate_detail->conn_sec); @@ -871,16 +942,13 @@ sub rate_prefix { } #should preserve (display?) this - my $charge_min = 0 - $included_min->{$regionnum}{$ratetimenum} - ( $conn_seconds / 60 ); - $included_min->{$regionnum}{$ratetimenum} = 0; - $charge += ($rate_detail->min_charge * $charge_min) if $charge_min > 0; #still not rounded - - } elsif ( ${$opt{region_group_included_min}} > 0 - && $region_group - && $rate_detail->region_group - ) - { - $included_min->{$regionnum}{$ratetimenum} = 0 + if ( $granularity == 0 ) { # per call rate + $charge += $rate_detail->min_charge; + } else { + my $charge_min = ( $charge_sec - $conn_seconds ) / 60; + $charge += ($rate_detail->min_charge * $charge_min) if $charge_min > 0; #still not rounded + } + } # choose next rate_detail @@ -897,9 +965,15 @@ sub rate_prefix { # this is why we need regionnum/rate_region.... warn " (rate region $rate_region)\n" if $DEBUG; + # NOW round it. + my $rounding = $part_pkg->option_cacheable('rounding') || 2; + my $sprintformat = '%.'. $rounding. 'f'; + my $roundup = 10**(-3-$rounding); + my $price = sprintf($sprintformat, $charge + $roundup); + $self->set_status_and_rated_price( 'rated', - sprintf('%.2f', $charge + 0.000001), # NOW round it. + $price, $opt{'svcnum'}, 'rated_pretty_dst' => $pretty_dst, 'rated_regionname' => $rate_region->regionname, @@ -1168,6 +1242,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 @@ -1182,7 +1258,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 @@ -1191,7 +1267,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, @@ -1199,14 +1275,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, @@ -1240,7 +1316,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', @@ -1248,7 +1324,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] ]; @@ -1286,8 +1362,6 @@ sub downstream_csv { #$opt{'money_char'} ||= $conf->config('money_char') || '$'; $opt{'money_char'} ||= FS::Conf->new->config('money_char') || '$'; - eval "use Text::CSV_XS;"; - die $@ if $@; my $csv = new Text::CSV_XS; my @columns = @@ -1578,6 +1652,11 @@ my %import_options = ( keys %cdr_info }, + 'format_asn_formats' => + { map { $_ => $cdr_info{$_}->{'asn_format'}; } + keys %cdr_info + }, + 'format_row_callbacks' => { map { $_ => $cdr_info{$_}->{'row_callback'}; } keys %cdr_info },