8 use FS::Record qw( qsearch qsearchs );
12 use FS::cdr_upstream_rate;
14 @ISA = qw(FS::Record);
18 FS::cdr - Object methods for cdr records
24 $record = new FS::cdr \%hash;
25 $record = new FS::cdr { 'column' => 'value' };
27 $error = $record->insert;
29 $error = $new_record->replace($old_record);
31 $error = $record->delete;
33 $error = $record->check;
37 An FS::cdr object represents an Call Data Record, typically from a telephony
38 system or provider of some sort. FS::cdr inherits from FS::Record. The
39 following fields are currently supported:
43 =item acctid - primary key
45 =item calldate - Call timestamp (SQL timestamp)
47 =item clid - Caller*ID with text
49 =item src - Caller*ID number / Source number
51 =item dst - Destination extension
53 =item dcontext - Destination context
55 =item channel - Channel used
57 =item dstchannel - Destination channel if appropriate
59 =item lastapp - Last application if appropriate
61 =item lastdata - Last application data
63 =item startdate - Start of call (UNIX-style integer timestamp)
65 =item answerdate - Answer time of call (UNIX-style integer timestamp)
67 =item enddate - End time of call (UNIX-style integer timestamp)
69 =item duration - Total time in system, in seconds
71 =item billsec - Total time call is up, in seconds
73 =item disposition - What happened to the call: ANSWERED, NO ANSWER, BUSY
75 =item amaflags - What flags to use: BILL, IGNORE etc, specified on a per channel basis like accountcode.
79 #ignore the "omit" and "documentation" AMAs??
80 #AMA = Automated Message Accounting.
81 #default: Sets the system default.
82 #omit: Do not record calls.
83 #billing: Mark the entry for billing
84 #documentation: Mark the entry for documentation.
88 =item accountcode - CDR account number to use: account
90 =item uniqueid - Unique channel identifier (Unitel/RSLCOM Event ID)
92 =item userfield - CDR user-defined field
94 =item cdr_type - CDR type - see L<FS::cdr_type> (Usage = 1, S&E = 7, OC&C = 8)
96 =item charged_party - Service number to be billed
98 =item upstream_currency - Wholesale currency from upstream
100 =item upstream_price - Wholesale price from upstream
102 =item upstream_rateplanid - Upstream rate plan ID
104 =item rated_price - Rated (or re-rated) price
106 =item distance - km (need units field?)
108 =item islocal - Local - 1, Non Local = 0
110 =item calltypenum - Type of call - see L<FS::cdr_calltype>
112 =item description - Description (cdr_type 7&8 only) (used for cust_bill_pkg.itemdesc)
114 =item quantity - Number of items (cdr_type 7&8 only)
116 =item carrierid - Upstream Carrier ID (see L<FS::cdr_carrier>)
120 #Telstra =1, Optus = 2, RSL COM = 3
122 =item upstream_rateid - Upstream Rate ID
124 =item svcnum - Link to customer service (see L<FS::cust_svc>)
126 =item freesidestatus - NULL, done (or something)
136 Creates a new CDR. To add the CDR to the database, see L<"insert">.
138 Note that this stores the hash reference, not a distinct copy of the hash it
139 points to. You can ask the object for a copy with the I<hash> method.
143 # the new method can be inherited from FS::Record, if a table method is defined
149 Adds this record to the database. If there is an error, returns the error,
150 otherwise returns false.
154 # the insert method can be inherited from FS::Record
158 Delete this record from the database.
162 # the delete method can be inherited from FS::Record
164 =item replace OLD_RECORD
166 Replaces the OLD_RECORD with this one in the database. If there is an error,
167 returns the error, otherwise returns false.
171 # the replace method can be inherited from FS::Record
175 Checks all fields to make sure this is a valid CDR. If there is
176 an error, returns the error, otherwise returns false. Called by the insert
179 Note: Unlike most types of records, we don't want to "reject" a CDR and we want
180 to process them as quickly as possible, so we allow the database to check most
188 # we don't want to "reject" a CDR like other sorts of input...
190 # $self->ut_numbern('acctid')
191 ## || $self->ut_('calldate')
192 # || $self->ut_text('clid')
193 # || $self->ut_text('src')
194 # || $self->ut_text('dst')
195 # || $self->ut_text('dcontext')
196 # || $self->ut_text('channel')
197 # || $self->ut_text('dstchannel')
198 # || $self->ut_text('lastapp')
199 # || $self->ut_text('lastdata')
200 # || $self->ut_numbern('startdate')
201 # || $self->ut_numbern('answerdate')
202 # || $self->ut_numbern('enddate')
203 # || $self->ut_number('duration')
204 # || $self->ut_number('billsec')
205 # || $self->ut_text('disposition')
206 # || $self->ut_number('amaflags')
207 # || $self->ut_text('accountcode')
208 # || $self->ut_text('uniqueid')
209 # || $self->ut_text('userfield')
210 # || $self->ut_numbern('cdrtypenum')
211 # || $self->ut_textn('charged_party')
212 ## || $self->ut_n('upstream_currency')
213 ## || $self->ut_n('upstream_price')
214 # || $self->ut_numbern('upstream_rateplanid')
215 ## || $self->ut_n('distance')
216 # || $self->ut_numbern('islocal')
217 # || $self->ut_numbern('calltypenum')
218 # || $self->ut_textn('description')
219 # || $self->ut_numbern('quantity')
220 # || $self->ut_numbern('carrierid')
221 # || $self->ut_numbern('upstream_rateid')
222 # || $self->ut_numbern('svcnum')
223 # || $self->ut_textn('freesidestatus')
225 # return $error if $error;
227 #check the foreign keys even?
228 #do we want to outright *reject* the CDR?
230 $self->ut_numbern('acctid')
232 #Usage = 1, S&E = 7, OC&C = 8
233 || $self->ut_foreign_keyn('cdrtypenum', 'cdr_type', 'cdrtypenum' )
235 #the big list in appendix 2
236 || $self->ut_foreign_keyn('calltypenum', 'cdr_calltype', 'calltypenum' )
238 # Telstra =1, Optus = 2, RSL COM = 3
239 || $self->ut_foreign_keyn('carrierid', 'cdr_carrier', 'carrierid' )
241 return $error if $error;
246 =item set_status_and_rated_price STATUS [ RATED_PRICE ]
248 Sets the status to the provided string. If there is an error, returns the
249 error, otherwise returns false.
253 sub set_status_and_rated_price {
254 my($self, $status, $rated_price) = @_;
255 $self->status($status);
256 $self->rated_price($rated_price);
262 Parses the calldate in SQL string format and returns a UNIX timestamp.
267 str2time(shift->calldate);
272 Returns the FS::cdr_carrier object associated with this CDR, or false if no
273 carrierid is defined.
277 my %carrier_cache = ();
281 return '' unless $self->carrierid;
282 $carrier_cache{$self->carrierid} ||=
283 qsearchs('cdr_carrier', { 'carrierid' => $self->carrierid } );
288 Returns the carrier name (see L<FS::cdr_carrier>), or the empty string if
289 no FS::cdr_carrier object is assocated with this CDR.
295 my $cdr_carrier = $self->cdr_carrier;
296 $cdr_carrier ? $cdr_carrier->carriername : '';
301 Returns the FS::cdr_calltype object associated with this CDR, or false if no
302 calltypenum is defined.
306 my %calltype_cache = ();
310 return '' unless $self->calltypenum;
311 $calltype_cache{$self->calltypenum} ||=
312 qsearchs('cdr_calltype', { 'calltypenum' => $self->calltypenum } );
317 Returns the call type name (see L<FS::cdr_calltype>), or the empty string if
318 no FS::cdr_calltype object is assocated with this CDR.
324 my $cdr_calltype = $self->cdr_calltype;
325 $cdr_calltype ? $cdr_calltype->calltypename : '';
328 =item cdr_upstream_rate
330 Returns the upstream rate mapping (see L<FS::cdr_upstream_rate>), or the empty
331 string if no FS::cdr_upstream_rate object is associated with this CDR.
335 sub cdr_upstream_rate {
337 return '' unless $self->upstream_rateid;
338 qsearchs('cdr_upstream_rate', { 'upstream_rateid' => $self->upstream_rateid })
342 =item _convergent_format COLUMN [ COUNTRYCODE ]
344 Returns the number in COLUMN formatted as follows:
346 If the country code does not match COUNTRYCODE (default "61"), it is returned
349 If the country code does match COUNTRYCODE (default "61"), it is removed. In
350 addiiton, "0" is prepended unless the number starts with 13, 18 or 19. (???)
354 sub _convergent_format {
355 my( $self, $field ) = ( shift, shift );
356 my $countrycode = scalar(@_) ? shift : '61'; #+61 = australia
357 #my $number = $self->$field();
358 my $number = $self->get($field);
359 #if ( $number =~ s/^(\+|011)$countrycode// ) {
360 if ( $number =~ s/^\+$countrycode// ) {
362 unless $number =~ /^1[389]/; #???
367 =item downstream_csv [ OPTION => VALUE, ... ]
371 my %export_formats = (
373 'carriername', #CARRIER
374 sub { shift->_convergent_format('src') }, #SERVICE_NUMBER
375 sub { shift->_convergent_format('charged_party') }, #CHARGED_NUMBER
376 sub { time2str('%Y-%m-%d', shift->calldate_unix ) }, #DATE
377 sub { time2str('%T', shift->calldate_unix ) }, #TIME
378 'billsec', #'duration', #DURATION
379 sub { shift->_convergent_format('dst') }, #NUMBER_DIALED
380 '', #XXX add (from prefixes in most recent email) #FROM_DESC
381 '', #XXX add (from prefixes in most recent email) #TO_DESC
382 'calltypename', #CLASS_CODE
383 'rated_price', #PRICE
384 sub { shift->rated_price ? 'Y' : 'N' }, #RATED
390 my( $self, %opt ) = @_;
392 my $format = $opt{'format'}; # 'convergent';
393 return "Unknown format $format" unless exists $export_formats{$format};
395 eval "use Text::CSV_XS;";
397 my $csv = new Text::CSV_XS;
401 ref($_) ? &{$_}($self) : $self->$_();
403 @{ $export_formats{$format} };
405 my $status = $csv->combine(@columns);
406 die "FS::CDR: error combining ". $csv->error_input(). "into downstream CSV"
423 my %import_formats = (
434 'startdate', # XXX will need massaging
448 'calldate', # may need massaging? huh maybe not...
449 #'billsec', #XXX duration and billsec?
450 sub { $_[0]->billsec( $_[1] );
451 $_[0]->duration( $_[1] );
454 'dst', # XXX needs to have "+61" prepended unless /^\+/ ???
458 'upstream_rateplanid',
462 'startdate', #XXX needs massaging
474 my $fh = $param->{filehandle};
475 my $format = $param->{format};
477 return "Unknown format $format" unless exists $import_formats{$format};
479 eval "use Text::CSV_XS;";
482 my $csv = new Text::CSV_XS;
487 local $SIG{HUP} = 'IGNORE';
488 local $SIG{INT} = 'IGNORE';
489 local $SIG{QUIT} = 'IGNORE';
490 local $SIG{TERM} = 'IGNORE';
491 local $SIG{TSTP} = 'IGNORE';
492 local $SIG{PIPE} = 'IGNORE';
494 my $oldAutoCommit = $FS::UID::AutoCommit;
495 local $FS::UID::AutoCommit = 0;
499 while ( defined($line=<$fh>) ) {
501 $csv->parse($line) or do {
502 $dbh->rollback if $oldAutoCommit;
503 return "can't parse: ". $csv->error_input();
506 my @columns = $csv->fields();
507 #warn join('-',@columns);
513 my $field_or_sub = $_;
514 if ( ref($field_or_sub) ) {
515 push @later, $field_or_sub, shift(@columns);
518 ( $field_or_sub => shift @columns );
522 @{ $import_formats{$format} }
525 my $cdr = new FS::cdr ( \%cdr );
527 while ( scalar(@later) ) {
528 my $sub = shift @later;
529 my $data = shift @later;
530 &{$sub}($cdr, $data); # $cdr->&{$sub}($data);
533 my $error = $cdr->insert;
535 $dbh->rollback if $oldAutoCommit;
545 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
547 #might want to disable this if we skip records for any reason...
548 return "Empty file!" unless $imported;
560 L<FS::Record>, schema.html from the base documentation.