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.
87 =item accountcode - CDR account number to use: account
89 =item uniqueid - Unique channel identifier (Unitel/RSLCOM Event ID)
91 =item userfield - CDR user-defined field
93 =item cdr_type - CDR type - see L<FS::cdr_type> (Usage = 1, S&E = 7, OC&C = 8)
95 =item charged_party - Service number to be billed
97 =item upstream_currency - Wholesale currency from upstream
99 =item upstream_price - Wholesale price from upstream
101 =item upstream_rateplanid - Upstream rate plan ID
103 =item rated_price - Rated (or re-rated) price
105 =item distance - km (need units field?)
107 =item islocal - Local - 1, Non Local = 0
109 =item calltypenum - Type of call - see L<FS::cdr_calltype>
111 =item description - Description (cdr_type 7&8 only) (used for cust_bill_pkg.itemdesc)
113 =item quantity - Number of items (cdr_type 7&8 only)
115 =item carrierid - Upstream Carrier ID (see L<FS::cdr_carrier>)
119 #Telstra =1, Optus = 2, RSL COM = 3
121 =item upstream_rateid - Upstream Rate ID
123 =item svcnum - Link to customer service (see L<FS::cust_svc>)
125 =item freesidestatus - NULL, done (or something)
135 Creates a new CDR. To add the CDR to the database, see L<"insert">.
137 Note that this stores the hash reference, not a distinct copy of the hash it
138 points to. You can ask the object for a copy with the I<hash> method.
142 # the new method can be inherited from FS::Record, if a table method is defined
148 Adds this record to the database. If there is an error, returns the error,
149 otherwise returns false.
153 # the insert method can be inherited from FS::Record
157 Delete this record from the database.
161 # the delete method can be inherited from FS::Record
163 =item replace OLD_RECORD
165 Replaces the OLD_RECORD with this one in the database. If there is an error,
166 returns the error, otherwise returns false.
170 # the replace method can be inherited from FS::Record
174 Checks all fields to make sure this is a valid CDR. If there is
175 an error, returns the error, otherwise returns false. Called by the insert
178 Note: Unlike most types of records, we don't want to "reject" a CDR and we want
179 to process them as quickly as possible, so we allow the database to check most
187 # we don't want to "reject" a CDR like other sorts of input...
189 # $self->ut_numbern('acctid')
190 ## || $self->ut_('calldate')
191 # || $self->ut_text('clid')
192 # || $self->ut_text('src')
193 # || $self->ut_text('dst')
194 # || $self->ut_text('dcontext')
195 # || $self->ut_text('channel')
196 # || $self->ut_text('dstchannel')
197 # || $self->ut_text('lastapp')
198 # || $self->ut_text('lastdata')
199 # || $self->ut_numbern('startdate')
200 # || $self->ut_numbern('answerdate')
201 # || $self->ut_numbern('enddate')
202 # || $self->ut_number('duration')
203 # || $self->ut_number('billsec')
204 # || $self->ut_text('disposition')
205 # || $self->ut_number('amaflags')
206 # || $self->ut_text('accountcode')
207 # || $self->ut_text('uniqueid')
208 # || $self->ut_text('userfield')
209 # || $self->ut_numbern('cdrtypenum')
210 # || $self->ut_textn('charged_party')
211 ## || $self->ut_n('upstream_currency')
212 ## || $self->ut_n('upstream_price')
213 # || $self->ut_numbern('upstream_rateplanid')
214 ## || $self->ut_n('distance')
215 # || $self->ut_numbern('islocal')
216 # || $self->ut_numbern('calltypenum')
217 # || $self->ut_textn('description')
218 # || $self->ut_numbern('quantity')
219 # || $self->ut_numbern('carrierid')
220 # || $self->ut_numbern('upstream_rateid')
221 # || $self->ut_numbern('svcnum')
222 # || $self->ut_textn('freesidestatus')
224 # return $error if $error;
226 $self->calldate( $self->startdate_sql )
227 if !$self->calldate && $self->startdate;
229 unless ( $self->charged_party ) {
230 if ( $self->dst =~ /^(\+?1)?8[02-8]{2}/ ) {
231 $self->charged_party($self->dst);
233 $self->charged_party($self->src);
237 #check the foreign keys even?
238 #do we want to outright *reject* the CDR?
240 $self->ut_numbern('acctid')
242 #Usage = 1, S&E = 7, OC&C = 8
243 || $self->ut_foreign_keyn('cdrtypenum', 'cdr_type', 'cdrtypenum' )
245 #the big list in appendix 2
246 || $self->ut_foreign_keyn('calltypenum', 'cdr_calltype', 'calltypenum' )
248 # Telstra =1, Optus = 2, RSL COM = 3
249 || $self->ut_foreign_keyn('carrierid', 'cdr_carrier', 'carrierid' )
251 return $error if $error;
256 =item set_status_and_rated_price STATUS [ RATED_PRICE ]
258 Sets the status to the provided string. If there is an error, returns the
259 error, otherwise returns false.
263 sub set_status_and_rated_price {
264 my($self, $status, $rated_price) = @_;
265 $self->freesidestatus($status);
266 $self->rated_price($rated_price);
272 Parses the calldate in SQL string format and returns a UNIX timestamp.
277 str2time(shift->calldate);
282 Parses the startdate in UNIX timestamp format and returns a string in SQL
288 my($sec,$min,$hour,$mday,$mon,$year) = localtime(shift->startdate);
291 "$year-$mon-$mday $hour:$min:$sec";
296 Returns the FS::cdr_carrier object associated with this CDR, or false if no
297 carrierid is defined.
301 my %carrier_cache = ();
305 return '' unless $self->carrierid;
306 $carrier_cache{$self->carrierid} ||=
307 qsearchs('cdr_carrier', { 'carrierid' => $self->carrierid } );
312 Returns the carrier name (see L<FS::cdr_carrier>), or the empty string if
313 no FS::cdr_carrier object is assocated with this CDR.
319 my $cdr_carrier = $self->cdr_carrier;
320 $cdr_carrier ? $cdr_carrier->carriername : '';
325 Returns the FS::cdr_calltype object associated with this CDR, or false if no
326 calltypenum is defined.
330 my %calltype_cache = ();
334 return '' unless $self->calltypenum;
335 $calltype_cache{$self->calltypenum} ||=
336 qsearchs('cdr_calltype', { 'calltypenum' => $self->calltypenum } );
341 Returns the call type name (see L<FS::cdr_calltype>), or the empty string if
342 no FS::cdr_calltype object is assocated with this CDR.
348 my $cdr_calltype = $self->cdr_calltype;
349 $cdr_calltype ? $cdr_calltype->calltypename : '';
352 =item cdr_upstream_rate
354 Returns the upstream rate mapping (see L<FS::cdr_upstream_rate>), or the empty
355 string if no FS::cdr_upstream_rate object is associated with this CDR.
359 sub cdr_upstream_rate {
361 return '' unless $self->upstream_rateid;
362 qsearchs('cdr_upstream_rate', { 'upstream_rateid' => $self->upstream_rateid })
366 =item _convergent_format COLUMN [ COUNTRYCODE ]
368 Returns the number in COLUMN formatted as follows:
370 If the country code does not match COUNTRYCODE (default "61"), it is returned
373 If the country code does match COUNTRYCODE (default "61"), it is removed. In
374 addiiton, "0" is prepended unless the number starts with 13, 18 or 19. (???)
378 sub _convergent_format {
379 my( $self, $field ) = ( shift, shift );
380 my $countrycode = scalar(@_) ? shift : '61'; #+61 = australia
381 #my $number = $self->$field();
382 my $number = $self->get($field);
383 #if ( $number =~ s/^(\+|011)$countrycode// ) {
384 if ( $number =~ s/^\+$countrycode// ) {
386 unless $number =~ /^1[389]/; #???
391 =item downstream_csv [ OPTION => VALUE, ... ]
395 my %export_formats = (
397 'carriername', #CARRIER
398 sub { shift->_convergent_format('src') }, #SERVICE_NUMBER
399 sub { shift->_convergent_format('charged_party') }, #CHARGED_NUMBER
400 sub { time2str('%Y-%m-%d', shift->calldate_unix ) }, #DATE
401 sub { time2str('%T', shift->calldate_unix ) }, #TIME
402 'billsec', #'duration', #DURATION
403 sub { shift->_convergent_format('dst') }, #NUMBER_DIALED
404 '', #XXX add (from prefixes in most recent email) #FROM_DESC
405 '', #XXX add (from prefixes in most recent email) #TO_DESC
406 'calltypename', #CLASS_CODE
407 'rated_price', #PRICE
408 sub { shift->rated_price ? 'Y' : 'N' }, #RATED
414 my( $self, %opt ) = @_;
416 my $format = $opt{'format'}; # 'convergent';
417 return "Unknown format $format" unless exists $export_formats{$format};
419 eval "use Text::CSV_XS;";
421 my $csv = new Text::CSV_XS;
425 ref($_) ? &{$_}($self) : $self->$_();
427 @{ $export_formats{$format} };
429 my $status = $csv->combine(@columns);
430 die "FS::CDR: error combining ". $csv->error_input(). "into downstream CSV"
445 Returns an ordered list of key value pairs containing import format names
446 as keys (for use with batch_import) and "pretty" format names as values.
452 'asterisk' => 'Asterisk',
454 'unitel' => 'Unitel/RSLCOM',
455 'simple' => 'Simple',
459 my($tmp_mday, $tmp_mon, $tmp_year);
461 sub _cdr_date_parser_maker {
464 my( $cdr, $date ) = @_;
465 $cdr->$field( _cdr_date_parse($date) );
469 sub _cdr_date_parse {
472 return '' unless length($date); #that's okay, it becomes NULL
474 #$date =~ /^\s*(\d{4})[\-\/]\(\d{1,2})[\-\/](\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s*$/
475 $date =~ /^\s*(\d{4})\D(\d{1,2})\D(\d{1,2})\s+(\d{1,2})\D(\d{1,2})\D(\d{1,2})\s*$/
476 or die "unparsable date: $date"; #maybe we shouldn't die...
477 my($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
479 timelocal($sec, $min, $hour, $day, $mon-1, $year);
482 #http://www.the-asterisk-book.com/unstable/funktionen-cdr.html
485 OMIT => 1, #asterisk 1.4+
486 IGNORE => 1, #asterisk 1.2
487 BILLING => 2, #asterisk 1.4+
488 BILL => 2, #asterisk 1.2
493 my %import_formats = (
504 _cdr_date_parser_maker('startdate'),
505 _cdr_date_parser_maker('answerdate'),
506 _cdr_date_parser_maker('enddate'),
510 sub { my($cdr, $amaflags) = @_; $cdr->amaflags($amaflags{$amaflags}); },
515 sub { my($cdr, $field) = @_; }, #RecordType
516 sub { my($cdr, $field) = @_; }, #all10#RecordVersion
517 sub { my($cdr, $field) = @_; }, #OrigShelfNumber
518 sub { my($cdr, $field) = @_; }, #OrigCardNumber
519 sub { my($cdr, $field) = @_; }, #OrigCircuit
520 sub { my($cdr, $field) = @_; }, #OrigCircuitType
521 sub { my($cdr, $field) = @_; }, #SequenceNumber
522 sub { my($cdr, $field) = @_; }, #SessionNumber
523 sub { my($cdr, $field) = @_; }, #CallingPartyNumber
524 sub { my($cdr, $field) = @_; }, #CalledPartyNumber
525 sub { my($cdr, $field) = @_; }, #CallArrivalTime
526 sub { my($cdr, $field) = @_; }, #CallCompletionTime
527 sub { my($cdr, $field) = @_; }, #Disposition
528 sub { my($cdr, $field) = @_; }, #DispositionTime
529 sub { my($cdr, $field) = @_; }, #TCAP
530 sub { my($cdr, $field) = @_; }, #OutboundCarrierConnectTime
531 sub { my($cdr, $field) = @_; }, #OutboundCarrierDisconnectTime
532 sub { my($cdr, $field) = @_; }, #TermTrunkGroup
533 sub { my($cdr, $field) = @_; }, #TermShelfNumber
534 sub { my($cdr, $field) = @_; }, #TermCardNumber
535 sub { my($cdr, $field) = @_; }, #TermCircuit
536 sub { my($cdr, $field) = @_; }, #TermCircuitType
537 sub { my($cdr, $field) = @_; }, #OutboundCarrierId
538 sub { my($cdr, $field) = @_; }, #BillingNumber
539 sub { my($cdr, $field) = @_; }, #SubscriberNumber
540 sub { my($cdr, $field) = @_; }, #ServiceName
541 sub { my($cdr, $field) = @_; }, #ChargeTime
542 sub { my($cdr, $field) = @_; }, #ServiceInformation
543 sub { my($cdr, $field) = @_; }, #FacilityInfo
544 sub { my($cdr, $field) = @_; }, #CallTraceTime
545 sub { my($cdr, $field) = @_; }, #all-1#UniqueIndicator
546 sub { my($cdr, $field) = @_; }, #all-1#PresentationIndicator
547 sub { my($cdr, $field) = @_; }, #empty#Pin
548 sub { my($cdr, $field) = @_; }, #CallType
549 sub { my($cdr, $field) = @_; }, #OrigRateCenter
550 sub { my($cdr, $field) = @_; }, #TermRateCenter
551 sub { my($cdr, $field) = @_; }, #OrigTrunkGroup
552 'userfield', #empty#UserDefined
553 sub { my($cdr, $field) = @_; }, #empty#PseudoDestinationNumber
554 sub { my($cdr, $field) = @_; }, #all-1#PseudoCarrierCode
555 sub { my($cdr, $field) = @_; }, #empty#PseudoANI
556 sub { my($cdr, $field) = @_; }, #all-1#PseudoFacilityInfo
557 sub { my($cdr, $field) = @_; }, #OrigDialedDigits
558 sub { my($cdr, $field) = @_; }, #all-1#OrigOutboundCarrier
559 sub { my($cdr, $field) = @_; }, #IncomingCarrierID
560 sub { my($cdr, $field) = @_; }, #JurisdictionInfo
561 sub { my($cdr, $field) = @_; }, #OrigDestDigits
562 sub { my($cdr, $field) = @_; }, #InsertTime
563 sub { my($cdr, $field) = @_; }, #key
564 sub { my($cdr, $field) = @_; }, #empty#AMALineNumber
565 sub { my($cdr, $field) = @_; }, #empty#AMAslpID
566 sub { my($cdr, $field) = @_; }, #empty#AMADigitsDialedWC
567 sub { my($cdr, $field) = @_; }, #OpxOffHook
568 sub { my($cdr, $field) = @_; }, #OpxOnHook
570 #acctid - primary key
571 #calldate - Call timestamp (SQL timestamp)
572 #clid - Caller*ID with text
573 #src - Caller*ID number / Source number
574 #dst - Destination extension
575 #dcontext - Destination context
576 #channel - Channel used
577 #dstchannel - Destination channel if appropriate
578 #lastapp - Last application if appropriate
579 #lastdata - Last application data
580 #startdate - Start of call (UNIX-style integer timestamp)
581 #answerdate - Answer time of call (UNIX-style integer timestamp)
582 #enddate - End time of call (UNIX-style integer timestamp)
583 #duration - Total time in system, in seconds
584 #billsec - Total time call is up, in seconds
585 #disposition - What happened to the call: ANSWERED, NO ANSWER, BUSY
586 #amaflags - What flags to use: BILL, IGNORE etc, specified on a per
587 #channel basis like accountcode.
588 #accountcode - CDR account number to use: account
589 #uniqueid - Unique channel identifier (Unitel/RSLCOM Event ID)
590 #userfield - CDR user-defined field
591 #cdr_type - CDR type - see FS::cdr_type (Usage = 1, S&E = 7, OC&C = 8)
592 #charged_party - Service number to be billed
593 #upstream_currency - Wholesale currency from upstream
594 #upstream_price - Wholesale price from upstream
595 #upstream_rateplanid - Upstream rate plan ID
596 #rated_price - Rated (or re-rated) price
597 #distance - km (need units field?)
598 #islocal - Local - 1, Non Local = 0
599 #calltypenum - Type of call - see FS::cdr_calltype
600 #description - Description (cdr_type 7&8 only) (used for
601 #cust_bill_pkg.itemdesc)
602 #quantity - Number of items (cdr_type 7&8 only)
603 #carrierid - Upstream Carrier ID (see FS::cdr_carrier)
604 #upstream_rateid - Upstream Rate ID
605 #svcnum - Link to customer service (see FS::cust_svc)
606 #freesidestatus - NULL, done (or something)
613 'calldate', # may need massaging? huh maybe not...
614 #'billsec', #XXX duration and billsec?
615 sub { $_[0]->billsec( $_[1] );
616 $_[0]->duration( $_[1] );
619 'dst', # XXX needs to have "+61" prepended unless /^\+/ ???
623 'upstream_rateplanid',
627 'startdate', #XXX needs massaging
637 sub { my($cdr, $date) = @_;
638 $date =~ /^(\d{1,2})\/(\d{1,2})\/(\d\d(\d\d)?)$/
639 or die "unparsable date: $date"; #maybe we shouldn't die...
640 #$cdr->startdate( timelocal(0, 0, 0 ,$2, $1-1, $3) );
641 ($tmp_mday, $tmp_mon, $tmp_year) = ( $2, $1-1, $3 );
645 sub { my($cdr, $time) = @_;
646 #my($sec, $min, $hour, $mday, $mon, $year)= localtime($cdr->startdate);
647 $time =~ /^(\d{1,2}):(\d{1,2}):(\d{1,2})$/
648 or die "unparsable time: $time"; #maybe we shouldn't die...
649 #$cdr->startdate( timelocal($3, $2, $1 ,$mday, $mon, $year) );
651 timelocal($3, $2, $1 ,$tmp_mday, $tmp_mon, $tmp_year)
662 sub { my($cdr, $min) = @_;
663 my $sec = sprintf('%.0f', $min * 60 );
664 $cdr->billsec( $sec );
665 $cdr->duration( $sec );
671 my %import_header = (
676 =item batch_import HASHREF
678 Imports CDR records. Available options are:
693 my $fh = $param->{filehandle};
694 my $format = $param->{format};
696 return "Unknown format $format" unless exists $import_formats{$format};
698 eval "use Text::CSV_XS;";
701 my $csv = new Text::CSV_XS;
706 local $SIG{HUP} = 'IGNORE';
707 local $SIG{INT} = 'IGNORE';
708 local $SIG{QUIT} = 'IGNORE';
709 local $SIG{TERM} = 'IGNORE';
710 local $SIG{TSTP} = 'IGNORE';
711 local $SIG{PIPE} = 'IGNORE';
713 my $oldAutoCommit = $FS::UID::AutoCommit;
714 local $FS::UID::AutoCommit = 0;
719 while ( defined($line=<$fh>) ) {
722 if ( ! $body++ && $import_header{'format'} && $line =~ /^[\w\, ]+$/ ) {
726 $csv->parse($line) or do {
727 $dbh->rollback if $oldAutoCommit;
728 return "can't parse: ". $csv->error_input();
731 my @columns = $csv->fields();
732 #warn join('-',@columns);
734 if ( $format eq 'simple' ) {
735 @columns = map { s/^ +//; $_; } @columns;
742 my $field_or_sub = $_;
743 if ( ref($field_or_sub) ) {
744 push @later, $field_or_sub, shift(@columns);
747 ( $field_or_sub => shift @columns );
751 @{ $import_formats{$format} }
754 my $cdr = new FS::cdr ( \%cdr );
756 while ( scalar(@later) ) {
757 my $sub = shift @later;
758 my $data = shift @later;
759 &{$sub}($cdr, $data); # $cdr->&{$sub}($data);
762 my $error = $cdr->insert;
764 $dbh->rollback if $oldAutoCommit;
774 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
776 #might want to disable this if we skip records for any reason...
777 return "Empty file!" unless $imported;
789 L<FS::Record>, schema.html from the base documentation.