9 use FS::Record qw( qsearch qsearchs );
13 use FS::cdr_upstream_rate;
15 @ISA = qw(FS::Record);
19 FS::cdr - Object methods for cdr records
25 $record = new FS::cdr \%hash;
26 $record = new FS::cdr { 'column' => 'value' };
28 $error = $record->insert;
30 $error = $new_record->replace($old_record);
32 $error = $record->delete;
34 $error = $record->check;
38 An FS::cdr object represents an Call Data Record, typically from a telephony
39 system or provider of some sort. FS::cdr inherits from FS::Record. The
40 following fields are currently supported:
44 =item acctid - primary key
46 =item calldate - Call timestamp (SQL timestamp)
48 =item clid - Caller*ID with text
50 =item src - Caller*ID number / Source number
52 =item dst - Destination extension
54 =item dcontext - Destination context
56 =item channel - Channel used
58 =item dstchannel - Destination channel if appropriate
60 =item lastapp - Last application if appropriate
62 =item lastdata - Last application data
64 =item startdate - Start of call (UNIX-style integer timestamp)
66 =item answerdate - Answer time of call (UNIX-style integer timestamp)
68 =item enddate - End time of call (UNIX-style integer timestamp)
70 =item duration - Total time in system, in seconds
72 =item billsec - Total time call is up, in seconds
74 =item disposition - What happened to the call: ANSWERED, NO ANSWER, BUSY
76 =item amaflags - What flags to use: BILL, IGNORE etc, specified on a per channel basis like accountcode.
80 #ignore the "omit" and "documentation" AMAs??
81 #AMA = Automated Message Accounting.
82 #default: Sets the system default.
83 #omit: Do not record calls.
84 #billing: Mark the entry for billing
85 #documentation: Mark the entry for documentation.
89 =item accountcode - CDR account number to use: account
91 =item uniqueid - Unique channel identifier (Unitel/RSLCOM Event ID)
93 =item userfield - CDR user-defined field
95 =item cdr_type - CDR type - see L<FS::cdr_type> (Usage = 1, S&E = 7, OC&C = 8)
97 =item charged_party - Service number to be billed
99 =item upstream_currency - Wholesale currency from upstream
101 =item upstream_price - Wholesale price from upstream
103 =item upstream_rateplanid - Upstream rate plan ID
105 =item rated_price - Rated (or re-rated) price
107 =item distance - km (need units field?)
109 =item islocal - Local - 1, Non Local = 0
111 =item calltypenum - Type of call - see L<FS::cdr_calltype>
113 =item description - Description (cdr_type 7&8 only) (used for cust_bill_pkg.itemdesc)
115 =item quantity - Number of items (cdr_type 7&8 only)
117 =item carrierid - Upstream Carrier ID (see L<FS::cdr_carrier>)
121 #Telstra =1, Optus = 2, RSL COM = 3
123 =item upstream_rateid - Upstream Rate ID
125 =item svcnum - Link to customer service (see L<FS::cust_svc>)
127 =item freesidestatus - NULL, done (or something)
137 Creates a new CDR. To add the CDR to the database, see L<"insert">.
139 Note that this stores the hash reference, not a distinct copy of the hash it
140 points to. You can ask the object for a copy with the I<hash> method.
144 # the new method can be inherited from FS::Record, if a table method is defined
150 Adds this record to the database. If there is an error, returns the error,
151 otherwise returns false.
155 # the insert method can be inherited from FS::Record
159 Delete this record from the database.
163 # the delete method can be inherited from FS::Record
165 =item replace OLD_RECORD
167 Replaces the OLD_RECORD with this one in the database. If there is an error,
168 returns the error, otherwise returns false.
172 # the replace method can be inherited from FS::Record
176 Checks all fields to make sure this is a valid CDR. If there is
177 an error, returns the error, otherwise returns false. Called by the insert
180 Note: Unlike most types of records, we don't want to "reject" a CDR and we want
181 to process them as quickly as possible, so we allow the database to check most
189 # we don't want to "reject" a CDR like other sorts of input...
191 # $self->ut_numbern('acctid')
192 ## || $self->ut_('calldate')
193 # || $self->ut_text('clid')
194 # || $self->ut_text('src')
195 # || $self->ut_text('dst')
196 # || $self->ut_text('dcontext')
197 # || $self->ut_text('channel')
198 # || $self->ut_text('dstchannel')
199 # || $self->ut_text('lastapp')
200 # || $self->ut_text('lastdata')
201 # || $self->ut_numbern('startdate')
202 # || $self->ut_numbern('answerdate')
203 # || $self->ut_numbern('enddate')
204 # || $self->ut_number('duration')
205 # || $self->ut_number('billsec')
206 # || $self->ut_text('disposition')
207 # || $self->ut_number('amaflags')
208 # || $self->ut_text('accountcode')
209 # || $self->ut_text('uniqueid')
210 # || $self->ut_text('userfield')
211 # || $self->ut_numbern('cdrtypenum')
212 # || $self->ut_textn('charged_party')
213 ## || $self->ut_n('upstream_currency')
214 ## || $self->ut_n('upstream_price')
215 # || $self->ut_numbern('upstream_rateplanid')
216 ## || $self->ut_n('distance')
217 # || $self->ut_numbern('islocal')
218 # || $self->ut_numbern('calltypenum')
219 # || $self->ut_textn('description')
220 # || $self->ut_numbern('quantity')
221 # || $self->ut_numbern('carrierid')
222 # || $self->ut_numbern('upstream_rateid')
223 # || $self->ut_numbern('svcnum')
224 # || $self->ut_textn('freesidestatus')
226 # return $error if $error;
228 $self->calldate( $self->startdate_sql )
229 if !$self->calldate && $self->startdate;
231 unless ( $self->charged_party ) {
232 if ( $self->dst =~ /^(\+?1)?8[02-8]{2}/ ) {
233 $self->charged_party($self->dst);
235 $self->charged_party($self->src);
239 #check the foreign keys even?
240 #do we want to outright *reject* the CDR?
242 $self->ut_numbern('acctid')
244 #Usage = 1, S&E = 7, OC&C = 8
245 || $self->ut_foreign_keyn('cdrtypenum', 'cdr_type', 'cdrtypenum' )
247 #the big list in appendix 2
248 || $self->ut_foreign_keyn('calltypenum', 'cdr_calltype', 'calltypenum' )
250 # Telstra =1, Optus = 2, RSL COM = 3
251 || $self->ut_foreign_keyn('carrierid', 'cdr_carrier', 'carrierid' )
253 return $error if $error;
258 =item set_status_and_rated_price STATUS [ RATED_PRICE ]
260 Sets the status to the provided string. If there is an error, returns the
261 error, otherwise returns false.
265 sub set_status_and_rated_price {
266 my($self, $status, $rated_price) = @_;
267 $self->freesidestatus($status);
268 $self->rated_price($rated_price);
274 Parses the calldate in SQL string format and returns a UNIX timestamp.
279 str2time(shift->calldate);
284 Parses the startdate in UNIX timestamp format and returns a string in SQL
290 my($sec,$min,$hour,$mday,$mon,$year) = localtime(shift->startdate);
293 "$year-$mon-$mday $hour:$min:$sec";
298 Returns the FS::cdr_carrier object associated with this CDR, or false if no
299 carrierid is defined.
303 my %carrier_cache = ();
307 return '' unless $self->carrierid;
308 $carrier_cache{$self->carrierid} ||=
309 qsearchs('cdr_carrier', { 'carrierid' => $self->carrierid } );
314 Returns the carrier name (see L<FS::cdr_carrier>), or the empty string if
315 no FS::cdr_carrier object is assocated with this CDR.
321 my $cdr_carrier = $self->cdr_carrier;
322 $cdr_carrier ? $cdr_carrier->carriername : '';
327 Returns the FS::cdr_calltype object associated with this CDR, or false if no
328 calltypenum is defined.
332 my %calltype_cache = ();
336 return '' unless $self->calltypenum;
337 $calltype_cache{$self->calltypenum} ||=
338 qsearchs('cdr_calltype', { 'calltypenum' => $self->calltypenum } );
343 Returns the call type name (see L<FS::cdr_calltype>), or the empty string if
344 no FS::cdr_calltype object is assocated with this CDR.
350 my $cdr_calltype = $self->cdr_calltype;
351 $cdr_calltype ? $cdr_calltype->calltypename : '';
354 =item cdr_upstream_rate
356 Returns the upstream rate mapping (see L<FS::cdr_upstream_rate>), or the empty
357 string if no FS::cdr_upstream_rate object is associated with this CDR.
361 sub cdr_upstream_rate {
363 return '' unless $self->upstream_rateid;
364 qsearchs('cdr_upstream_rate', { 'upstream_rateid' => $self->upstream_rateid })
368 =item _convergent_format COLUMN [ COUNTRYCODE ]
370 Returns the number in COLUMN formatted as follows:
372 If the country code does not match COUNTRYCODE (default "61"), it is returned
375 If the country code does match COUNTRYCODE (default "61"), it is removed. In
376 addiiton, "0" is prepended unless the number starts with 13, 18 or 19. (???)
380 sub _convergent_format {
381 my( $self, $field ) = ( shift, shift );
382 my $countrycode = scalar(@_) ? shift : '61'; #+61 = australia
383 #my $number = $self->$field();
384 my $number = $self->get($field);
385 #if ( $number =~ s/^(\+|011)$countrycode// ) {
386 if ( $number =~ s/^\+$countrycode// ) {
388 unless $number =~ /^1[389]/; #???
393 =item downstream_csv [ OPTION => VALUE, ... ]
397 my %export_formats = (
399 'carriername', #CARRIER
400 sub { shift->_convergent_format('src') }, #SERVICE_NUMBER
401 sub { shift->_convergent_format('charged_party') }, #CHARGED_NUMBER
402 sub { time2str('%Y-%m-%d', shift->calldate_unix ) }, #DATE
403 sub { time2str('%T', shift->calldate_unix ) }, #TIME
404 'billsec', #'duration', #DURATION
405 sub { shift->_convergent_format('dst') }, #NUMBER_DIALED
406 '', #XXX add (from prefixes in most recent email) #FROM_DESC
407 '', #XXX add (from prefixes in most recent email) #TO_DESC
408 'calltypename', #CLASS_CODE
409 'rated_price', #PRICE
410 sub { shift->rated_price ? 'Y' : 'N' }, #RATED
416 my( $self, %opt ) = @_;
418 my $format = $opt{'format'}; # 'convergent';
419 return "Unknown format $format" unless exists $export_formats{$format};
421 eval "use Text::CSV_XS;";
423 my $csv = new Text::CSV_XS;
427 ref($_) ? &{$_}($self) : $self->$_();
429 @{ $export_formats{$format} };
431 my $status = $csv->combine(@columns);
432 die "FS::CDR: error combining ". $csv->error_input(). "into downstream CSV"
449 my($tmp_mday, $tmp_mon, $tmp_year);
451 my %import_formats = (
462 'startdate', # XXX will need massaging
476 'calldate', # may need massaging? huh maybe not...
477 #'billsec', #XXX duration and billsec?
478 sub { $_[0]->billsec( $_[1] );
479 $_[0]->duration( $_[1] );
482 'dst', # XXX needs to have "+61" prepended unless /^\+/ ???
486 'upstream_rateplanid',
490 'startdate', #XXX needs massaging
500 sub { my($cdr, $date) = @_;
501 $date =~ /^(\d{1,2})\/(\d{1,2})\/(\d\d(\d\d)?)$/
502 or die "unparsable date: $date"; #maybe we shouldn't die...
503 #$cdr->startdate( timelocal(0, 0, 0 ,$2, $1-1, $3) );
504 ($tmp_mday, $tmp_mon, $tmp_year) = ( $2, $1-1, $3 );
508 sub { my($cdr, $time) = @_;
509 #my($sec, $min, $hour, $mday, $mon, $year)= localtime($cdr->startdate);
510 $time =~ /^(\d{1,2}):(\d{1,2}):(\d{1,2})$/
511 or die "unparsable time: $time"; #maybe we shouldn't die...
512 #$cdr->startdate( timelocal($3, $2, $1 ,$mday, $mon, $year) );
514 timelocal($3, $2, $1 ,$tmp_mday, $tmp_mon, $tmp_year)
525 sub { my($cdr, $min) = @_;
526 my $sec = sprintf('%.0f', $min * 60 );
527 $cdr->billsec( $sec );
528 $cdr->duration( $sec );
537 my $fh = $param->{filehandle};
538 my $format = $param->{format};
540 return "Unknown format $format" unless exists $import_formats{$format};
542 eval "use Text::CSV_XS;";
545 my $csv = new Text::CSV_XS;
550 local $SIG{HUP} = 'IGNORE';
551 local $SIG{INT} = 'IGNORE';
552 local $SIG{QUIT} = 'IGNORE';
553 local $SIG{TERM} = 'IGNORE';
554 local $SIG{TSTP} = 'IGNORE';
555 local $SIG{PIPE} = 'IGNORE';
557 my $oldAutoCommit = $FS::UID::AutoCommit;
558 local $FS::UID::AutoCommit = 0;
561 if ( $format eq 'ams' ) { # and other formats with a header too?
567 while ( defined($line=<$fh>) ) {
570 if ( ! $body++ && $format eq 'ams' && $line =~ /^[\w\, ]+$/ ) {
574 $csv->parse($line) or do {
575 $dbh->rollback if $oldAutoCommit;
576 return "can't parse: ". $csv->error_input();
579 my @columns = $csv->fields();
580 #warn join('-',@columns);
582 if ( $format eq 'ams' ) {
583 @columns = map { s/^ +//; $_; } @columns;
590 my $field_or_sub = $_;
591 if ( ref($field_or_sub) ) {
592 push @later, $field_or_sub, shift(@columns);
595 ( $field_or_sub => shift @columns );
599 @{ $import_formats{$format} }
602 my $cdr = new FS::cdr ( \%cdr );
604 while ( scalar(@later) ) {
605 my $sub = shift @later;
606 my $data = shift @later;
607 &{$sub}($cdr, $data); # $cdr->&{$sub}($data);
610 my $error = $cdr->insert;
612 $dbh->rollback if $oldAutoCommit;
622 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
624 #might want to disable this if we skip records for any reason...
625 return "Empty file!" unless $imported;
637 L<FS::Record>, schema.html from the base documentation.