package FS::cdr;
use strict;
-use vars qw( @ISA @EXPORT_OK $DEBUG );
+use vars qw( @ISA @EXPORT_OK $DEBUG $me );
use Exporter;
use Tie::IxHash;
use Date::Parse;
use FS::cdr_type;
use FS::cdr_calltype;
use FS::cdr_carrier;
-use FS::cdr_upstream_rate;
+use FS::cdr_batch;
+use FS::cdr_termination;
@ISA = qw(FS::Record);
@EXPORT_OK = qw( _cdr_date_parser_maker _cdr_min_parser_maker );
$DEBUG = 0;
+$me = '[FS::cdr]';
=head1 NAME
'svcnum' => 'Freeside service',
'freesidestatus' => 'Freeside status',
'freesiderewritestatus' => 'Freeside rewrite status',
- 'cdrbatch' => 'Batch',
+ 'cdrbatch' => 'Legacy batch',
+ 'cdrbatchnum' => 'Batch',
},
};
# ;
# return $error if $error;
+ for my $f ( grep { $self->$_ =~ /[a-z ]/i } qw( startdate enddate ) ) {
+ $self->$f( str2time($self->$f) );
+ }
+
$self->calldate( $self->startdate_sql )
if !$self->calldate && $self->startdate;
$self->SUPER::check;
}
-=item is_tollfree
+=item is_tollfree [ COLUMN ]
+
+Returns true when the cdr represents a toll free number and false otherwise.
- Returns true when the cdr represents a toll free number and false otherwise.
+By default, inspects the dst field, but an optional column name can be passed
+to inspect other field.
=cut
sub is_tollfree {
my $self = shift;
- ( $self->dst =~ /^(\+?1)?8(8|([02-7])\3)/ ) ? 1 : 0;
+ my $field = scalar(@_) ? shift : 'dst';
+ ( $self->$field() =~ /^(\+?1)?8(8|([02-7])\3)/ ) ? 1 : 0;
}
=item set_charged_party
if $conf->exists('cdr-charged_party-accountcode-trim_leading_0s');
$self->charged_party( $charged_party );
+ } elsif ( $conf->exists('cdr-charged_party-field') ) {
+
+ my $field = $conf->config('cdr-charged_party-field');
+ $self->charged_party( $self->$field() );
+
} else {
if ( $self->is_tollfree ) {
}
-=item set_status_and_rated_price STATUS [ RATED_PRICE ]
+=item set_status_and_rated_price STATUS [ RATED_PRICE [ SVCNUM ] ]
Sets the status to the provided string. If there is an error, returns the
error, otherwise returns false.
=cut
sub set_status_and_rated_price {
- my($self, $status, $rated_price) = @_;
- $self->freesidestatus($status);
- $self->rated_price($rated_price);
- $self->replace();
+ my($self, $status, $rated_price, $svcnum, %opt) = @_;
+ if($opt{'inbound'}) {
+ my $term = qsearchs('cdr_termination', {
+ acctid => $self->acctid,
+ termpart => 1 # inbound
+ });
+ my $error;
+ if($term) {
+ warn "replacing existing cdr status (".$self->acctid.")\n" if $term;
+ $error = $term->delete;
+ return $error if $error;
+ }
+ $term = FS::cdr_termination->new({
+ acctid => $self->acctid,
+ termpart => 1,
+ rated_price => $rated_price,
+ status => $status,
+ svcnum => $svcnum,
+ });
+ return $term->insert;
+ }
+ else {
+ $self->freesidestatus($status);
+ $self->rated_price($rated_price);
+ $self->svcnum($svcnum) if $svcnum;
+ return $self->replace();
+ }
}
=item calldate_unix
},
);
-my $duration_sub = sub {
- my($cdr, %opt) = @_;
- if ( $opt{minutes} ) {
- $opt{minutes}. ( $opt{granularity} ? 'm' : ' call' );
- } else {
- sprintf('%.2fm', $cdr->billsec / 60 );
- }
-};
-
-my %export_formats = (
- 'simple' => [
- sub { time2str('%D', shift->calldate_unix ) }, #DATE
- sub { time2str('%r', shift->calldate_unix ) }, #TIME
- 'userfield', #USER
- 'dst', #NUMBER_DIALED
- $duration_sub, #DURATION
- #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
- sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
- ],
- 'simple2' => [
- sub { time2str('%D', shift->calldate_unix ) }, #DATE
- sub { time2str('%r', shift->calldate_unix ) }, #TIME
- #'userfield', #USER
- 'src', #called from
- 'dst', #NUMBER_DIALED
- $duration_sub, #DURATION
- #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
- sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
- ],
- 'default' => [
-
- #DATE
- sub { time2str('%D', shift->calldate_unix ) },
- # #time2str("%Y %b %d - %r", $cdr->calldate_unix ),
-
- #TIME
- sub { time2str('%r', shift->calldate_unix ) },
- # time2str("%c", $cdr->calldate_unix), #XXX this should probably be a config option dropdown so they can select US vs- rest of world dates or whatnot
-
- #DEST ("Number")
- sub { my($cdr, %opt) = @_; $opt{pretty_dst} || $cdr->dst; },
-
- #REGIONNAME ("Destination")
- sub { my($cdr, %opt) = @_; $opt{dst_regionname}; },
-
- #DURATION
- $duration_sub,
-
- #PRICE
- sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; },
-
- ],
-);
-$export_formats{'source_default'} = [ 'src', @{ $export_formats{'default'} }, ];
-$export_formats{'accountcode_default'} =
- [ @{ $export_formats{'default'} }[0,1],
- 'accountcode',
- @{ $export_formats{'default'} }[2..5],
- ];
+my %export_formats = ();
+sub export_formats {
+ #my $self = shift;
+
+ return %export_formats if keys %export_formats;
+
+ my $conf = new FS::Conf;
+ my $date_format = $conf->config('date_format') || '%m/%d/%Y';
+
+ my $duration_sub = sub {
+ my($cdr, %opt) = @_;
+ if ( $opt{minutes} ) {
+ $opt{minutes}. ( $opt{granularity} ? 'm' : ' call' );
+ } else {
+ #config if anyone really wants decimal minutes back
+ #sprintf('%.2fm', $cdr->billsec / 60 );
+ int($cdr->billsec / 60).'m '. ($cdr->billsec % 60).'s';
+ }
+ };
+
+ %export_formats = (
+ 'simple' => [
+ sub { time2str($date_format, shift->calldate_unix ) }, #DATE
+ sub { time2str('%r', shift->calldate_unix ) }, #TIME
+ 'userfield', #USER
+ 'dst', #NUMBER_DIALED
+ $duration_sub, #DURATION
+ #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
+ sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
+ ],
+ 'simple2' => [
+ sub { time2str($date_format, shift->calldate_unix ) }, #DATE
+ sub { time2str('%r', shift->calldate_unix ) }, #TIME
+ #'userfield', #USER
+ 'src', #called from
+ 'dst', #NUMBER_DIALED
+ $duration_sub, #DURATION
+ #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
+ sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
+ ],
+ 'default' => [
+
+ #DATE
+ sub { time2str($date_format, shift->calldate_unix ) },
+ # #time2str("%Y %b %d - %r", $cdr->calldate_unix ),
+
+ #TIME
+ sub { time2str('%r', shift->calldate_unix ) },
+ # time2str("%c", $cdr->calldate_unix), #XXX this should probably be a config option dropdown so they can select US vs- rest of world dates or whatnot
+
+ #DEST ("Number")
+ sub { my($cdr, %opt) = @_; $opt{pretty_dst} || $cdr->dst; },
+
+ #REGIONNAME ("Destination")
+ sub { my($cdr, %opt) = @_; $opt{dst_regionname}; },
+
+ #DURATION
+ $duration_sub,
+
+ #PRICE
+ sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; },
+
+ ],
+ );
+ $export_formats{'source_default'} = [ 'src', @{ $export_formats{'default'} }, ];
+ $export_formats{'accountcode_default'} =
+ [ @{ $export_formats{'default'} }[0,1],
+ 'accountcode',
+ @{ $export_formats{'default'} }[2..5],
+ ];
+
+ %export_formats
+}
sub downstream_csv {
my( $self, %opt ) = @_;
my $format = $opt{'format'};
- return "Unknown format $format" unless exists $export_formats{$format};
+ my %formats = $self->export_formats;
+ return "Unknown format $format" unless exists $formats{$format};
#my $conf = new FS::Conf;
#$opt{'money_char'} ||= $conf->config('money_char') || '$';
map {
ref($_) ? &{$_}($self, %opt) : $self->$_();
}
- @{ $export_formats{$format} };
+ @{ $formats{$format} };
my $status = $csv->combine(@columns);
die "FS::CDR: error combining ". $csv->error_input(). "into downstream CSV"
($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
} elsif ( $date =~ /^\s*(\d{1,2})\D(\d{1,2})\D(\d{4})\s+(\d{1,2})\D(\d{1,2})\D(\d{1,2})(\D|$)/ ) {
($mon, $day, $year, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
+ } elsif ( $date =~ /^\s*(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d+\.\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|$)/ ) {
+ # Taqua OM: 20050422203450943
+ ($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})$/ ) {
+ # WIP: 20100329121420
+ ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
} else {
die "unparsable date: $date"; #maybe we shouldn't die...
}
=cut
my %import_options = (
- 'table' => 'cdr',
+ 'table' => 'cdr',
+
+ 'batch_keycol' => 'cdrbatchnum',
+ 'batch_table' => 'cdr_batch',
+ 'batch_namecol' => 'cdrbatch',
'formats' => { map { $_ => $cdr_info{$_}->{'import_fields'}; }
keys %cdr_info
{ map { $_ => $cdr_info{$_}->{'fixedlength_format'}; }
keys %cdr_info
},
+
+ 'format_row_callbacks' => { map { $_ => $cdr_info{$_}->{'row_callback'}; }
+ keys %cdr_info
+ },
);
sub _import_options {
my $job = shift;
my $opt = _import_options;
- $opt->{'params'} = [ 'format', 'cdrbatch' ];
+# $opt->{'params'} = [ 'format', 'cdrbatch' ];
FS::Record::process_batch_import( $job, $opt, @_ );
# @columns = map { s/^ +//; $_; } @columns;
# }
+# _ upgrade_data
+#
+# Used by FS::Upgrade to migrate to a new database.
+
+sub _upgrade_data {
+ my ($class, %opts) = @_;
+
+ warn "$me upgrading $class\n" if $DEBUG;
+
+ my $sth = dbh->prepare(
+ 'SELECT DISTINCT(cdrbatch) FROM cdr WHERE cdrbatch IS NOT NULL'
+ ) or die dbh->errstr;
+
+ $sth->execute or die $sth->errstr;
+
+ my %cdrbatchnum = ();
+ while (my $row = $sth->fetchrow_arrayref) {
+
+ my $cdr_batch = qsearchs( 'cdr_batch', { 'cdrbatch' => $row->[0] } );
+ unless ( $cdr_batch ) {
+ $cdr_batch = new FS::cdr_batch { 'cdrbatch' => $row->[0] };
+ my $error = $cdr_batch->insert;
+ die $error if $error;
+ }
+
+ $cdrbatchnum{$row->[0]} = $cdr_batch->cdrbatchnum;
+ }
+
+ $sth = dbh->prepare('UPDATE cdr SET cdrbatch = NULL, cdrbatchnum = ? WHERE cdrbatch IS NOT NULL AND cdrbatch = ?') or die dbh->errstr;
+
+ foreach my $cdrbatch (keys %cdrbatchnum) {
+ $sth->execute($cdrbatchnum{$cdrbatch}, $cdrbatch) or die $sth->errstr;
+ }
+
+}
+
=back
=head1 BUGS