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
411 'voxlinesystems' => [
412 sub { time2str('%D', shift->calldate_unix ) }, #DATE
413 sub { time2str('%T', shift->calldate_unix ) }, #TIME
415 'dst', #NUMBER_DIALED
416 sub { sprintf('%.2fm', shift->billsec / 60 ) }, #DURATION
417 sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
422 my( $self, %opt ) = @_;
424 my $format = $opt{'format'}; # 'convergent';
425 return "Unknown format $format" unless exists $export_formats{$format};
427 eval "use Text::CSV_XS;";
429 my $csv = new Text::CSV_XS;
433 ref($_) ? &{$_}($self) : $self->$_();
435 @{ $export_formats{$format} };
437 my $status = $csv->combine(@columns);
438 die "FS::CDR: error combining ". $csv->error_input(). "into downstream CSV"
453 Returns an ordered list of key value pairs containing import format names
454 as keys (for use with batch_import) and "pretty" format names as values.
460 'asterisk' => 'Asterisk',
462 'unitel' => 'Unitel/RSLCOM',
463 'voxlinesystems' => 'VoxLineSystems', #XXX? get the actual vendor name
464 'simple' => 'Simple',
468 my($tmp_mday, $tmp_mon, $tmp_year);
470 sub _cdr_date_parser_maker {
473 my( $cdr, $date ) = @_;
474 #$cdr->$field( _cdr_date_parse($date) );
475 eval { $cdr->$field( _cdr_date_parse($date) ); };
476 die "error parsing date for $field from $date: $@\n" if $@;
480 sub _cdr_date_parse {
483 return '' unless length($date); #that's okay, it becomes NULL
485 #$date =~ /^\s*(\d{4})[\-\/]\(\d{1,2})[\-\/](\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s*$/
486 $date =~ /^\s*(\d{4})\D(\d{1,2})\D(\d{1,2})\s+(\d{1,2})\D(\d{1,2})\D(\d{1,2})(\D|$)/
487 or die "unparsable date: $date"; #maybe we shouldn't die...
488 my($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
490 return '' if $year == 1900 && $mon == 1 && $day == 1
491 && $hour == 0 && $min == 0 && $sec == 0;
493 timelocal($sec, $min, $hour, $day, $mon-1, $year);
496 #taqua #2007-10-31 08:57:24.113000000
498 #http://www.the-asterisk-book.com/unstable/funktionen-cdr.html
501 OMIT => 1, #asterisk 1.4+
502 IGNORE => 1, #asterisk 1.2
503 BILLING => 2, #asterisk 1.4+
504 BILL => 2, #asterisk 1.2
509 my %import_formats = (
520 _cdr_date_parser_maker('startdate'),
521 _cdr_date_parser_maker('answerdate'),
522 _cdr_date_parser_maker('enddate'),
526 sub { my($cdr, $amaflags) = @_; $cdr->amaflags($amaflags{$amaflags}); },
530 'taqua' => [ #some of these are kind arbitrary...
532 sub { my($cdr, $field) = @_; }, #XXX interesting RecordType
533 # easy to fix: Can't find cdr.cdrtypenum 1 in cdr_type.cdrtypenum
535 sub { my($cdr, $field) = @_; }, #all10#RecordVersion
536 sub { my($cdr, $field) = @_; }, #OrigShelfNumber
537 sub { my($cdr, $field) = @_; }, #OrigCardNumber
538 sub { my($cdr, $field) = @_; }, #OrigCircuit
539 sub { my($cdr, $field) = @_; }, #OrigCircuitType
540 'uniqueid', #SequenceNumber
541 'accountcode', #SessionNumber
542 'src', #CallingPartyNumber
543 'dst', #CalledPartyNumber
544 _cdr_date_parser_maker('startdate'), #CallArrivalTime
545 _cdr_date_parser_maker('enddate'), #CallCompletionTime
548 #sub { my($cdr, $d ) = @_; $cdr->disposition( $disposition{$d}): },
561 _cdr_date_parser_maker('answerdate'), #DispositionTime
562 sub { my($cdr, $field) = @_; }, #TCAP
563 sub { my($cdr, $field) = @_; }, #OutboundCarrierConnectTime
564 sub { my($cdr, $field) = @_; }, #OutboundCarrierDisconnectTime
567 #it appears channels are actually part of trunk groups, but this data
568 #is interesting and we need a source and destination place to put it
569 'dstchannel', #TermTrunkGroup
572 sub { my($cdr, $field) = @_; }, #TermShelfNumber
573 sub { my($cdr, $field) = @_; }, #TermCardNumber
574 sub { my($cdr, $field) = @_; }, #TermCircuit
575 sub { my($cdr, $field) = @_; }, #TermCircuitType
576 sub { my($cdr, $field) = @_; }, #OutboundCarrierId
577 'charged_party', #BillingNumber
578 sub { my($cdr, $field) = @_; }, #SubscriberNumber
579 'lastapp', #ServiceName
580 sub { my($cdr, $field) = @_; }, #some weirdness #ChargeTime
581 'lastdata', #ServiceInformation
582 sub { my($cdr, $field) = @_; }, #FacilityInfo
583 sub { my($cdr, $field) = @_; }, #all 1900-01-01 0#CallTraceTime
584 sub { my($cdr, $field) = @_; }, #all-1#UniqueIndicator
585 sub { my($cdr, $field) = @_; }, #all-1#PresentationIndicator
586 sub { my($cdr, $field) = @_; }, #empty#Pin
587 sub { my($cdr, $field) = @_; }, #CallType
588 sub { my($cdr, $field) = @_; }, #Balt/empty #OrigRateCenter
589 sub { my($cdr, $field) = @_; }, #Balt/empty #TermRateCenter
592 #it appears channels are actually part of trunk groups, but this data
593 #is interesting and we need a source and destination place to put it
594 'channel', #OrigTrunkGroup
596 'userfield', #empty#UserDefined
597 sub { my($cdr, $field) = @_; }, #empty#PseudoDestinationNumber
598 sub { my($cdr, $field) = @_; }, #all-1#PseudoCarrierCode
599 sub { my($cdr, $field) = @_; }, #empty#PseudoANI
600 sub { my($cdr, $field) = @_; }, #all-1#PseudoFacilityInfo
601 sub { my($cdr, $field) = @_; }, #OrigDialedDigits
602 sub { my($cdr, $field) = @_; }, #all-1#OrigOutboundCarrier
603 sub { my($cdr, $field) = @_; }, #IncomingCarrierID
604 'dcontext', #JurisdictionInfo
605 sub { my($cdr, $field) = @_; }, #OrigDestDigits
606 sub { my($cdr, $field) = @_; }, #huh?#InsertTime
607 sub { my($cdr, $field) = @_; }, #key
608 sub { my($cdr, $field) = @_; }, #empty#AMALineNumber
609 sub { my($cdr, $field) = @_; }, #empty#AMAslpID
610 sub { my($cdr, $field) = @_; }, #empty#AMADigitsDialedWC
611 sub { my($cdr, $field) = @_; }, #OpxOffHook
612 sub { my($cdr, $field) = @_; }, #OpxOnHook
614 #acctid - primary key
615 #AUTO #calldate - Call timestamp (SQL timestamp)
616 #clid - Caller*ID with text
617 #XXX src - Caller*ID number / Source number
618 #XXX dst - Destination extension
619 #dcontext - Destination context
620 #channel - Channel used
621 #dstchannel - Destination channel if appropriate
622 #lastapp - Last application if appropriate
623 #lastdata - Last application data
624 #startdate - Start of call (UNIX-style integer timestamp)
625 #answerdate - Answer time of call (UNIX-style integer timestamp)
626 #enddate - End time of call (UNIX-style integer timestamp)
627 #HACK#duration - Total time in system, in seconds
628 #HACK#XXX billsec - Total time call is up, in seconds
629 #disposition - What happened to the call: ANSWERED, NO ANSWER, BUSY
630 #INT amaflags - What flags to use: BILL, IGNORE etc, specified on a per channel basis like accountcode.
631 #accountcode - CDR account number to use: account
633 #uniqueid - Unique channel identifier (Unitel/RSLCOM Event ID)
634 #userfield - CDR user-defined field
636 #X cdrtypenum - CDR type - see FS::cdr_type (Usage = 1, S&E = 7, OC&C = 8)
637 #XXX charged_party - Service number to be billed
638 #upstream_currency - Wholesale currency from upstream
639 #X upstream_price - Wholesale price from upstream
640 #upstream_rateplanid - Upstream rate plan ID
641 #rated_price - Rated (or re-rated) price
642 #distance - km (need units field?)
643 #islocal - Local - 1, Non Local = 0
644 #calltypenum - Type of call - see FS::cdr_calltype
645 #X description - Description (cdr_type 7&8 only) (used for cust_bill_pkg.itemdesc)
646 #quantity - Number of items (cdr_type 7&8 only)
647 #carrierid - Upstream Carrier ID (see FS::cdr_carrier)
648 #upstream_rateid - Upstream Rate ID
650 #svcnum - Link to customer service (see FS::cust_svc)
651 #freesidestatus - NULL, done (or something)
658 'calldate', # may need massaging? huh maybe not...
659 #'billsec', #XXX duration and billsec?
660 sub { $_[0]->billsec( $_[1] );
661 $_[0]->duration( $_[1] );
664 'dst', # XXX needs to have "+61" prepended unless /^\+/ ???
668 'upstream_rateplanid',
672 'startdate', #XXX needs massaging
679 'voxlinesystems' => [ #XXX get the actual vendor name
680 'disposition', #Status
681 'startdate', #Start (what do you know, a timestamp!
682 sub { my($cdr, $field) = @_; }, #Start date
683 sub { my($cdr, $field) = @_; }, #Start time
684 'enddate', #End (also a timestamp!)
685 sub { my($cdr, $field) = @_; }, #End date
686 sub { my($cdr, $field) = @_; }, #End time
687 'accountcode', #Calling customer XXX map to agent_custid??
688 sub { my($cdr, $field) = @_; }, #Calling type
689 sub { shift->src('30000'); }, #XXX FAKE XXX 'src', #Calling number
690 'userfield', #Calling name #?
691 sub { my($cdr, $field) = @_; }, #Called type
692 'dst', #Called number
693 sub { my($cdr, $field) = @_; }, #Destination customer
694 sub { my($cdr, $field) = @_; }, #Destination type
695 sub { my($cdr, $field) = @_; }, #Destination Number
696 sub { my($cdr, $field) = @_; }, #Inbound calling type
697 sub { my($cdr, $field) = @_; }, #Inbound calling number
698 sub { my($cdr, $field) = @_; }, #Inbound called type
699 sub { my($cdr, $field) = @_; }, #Inbound called number
700 sub { my($cdr, $field) = @_; }, #Inbound destination type
701 sub { my($cdr, $field) = @_; }, #Inbound destination number
702 sub { my($cdr, $field) = @_; }, #Outbound calling type
703 sub { my($cdr, $field) = @_; }, #Outbound calling number
704 sub { my($cdr, $field) = @_; }, #Outbound called type
705 sub { my($cdr, $field) = @_; }, #Outbound called number
706 sub { my($cdr, $field) = @_; }, #Outbound destination type
707 sub { my($cdr, $field) = @_; }, #Outbound destination number
708 sub { my($cdr, $field) = @_; }, #Internal calling type
709 sub { my($cdr, $field) = @_; }, #Internal calling number
710 sub { my($cdr, $field) = @_; }, #Internal called type
711 sub { my($cdr, $field) = @_; }, #Internal called number
712 sub { my($cdr, $field) = @_; }, #Internal destination type
713 sub { my($cdr, $field) = @_; }, #Internal destination number
714 'duration', #Total seconds
715 sub { my($cdr, $field) = @_; }, #Ring seconds
716 'billsec', #Billable seconds
717 'upstream_price', #Cost
718 sub { my($cdr, $field) = @_; }, #Billing customer
719 sub { my($cdr, $field) = @_; }, #Billing customer name
720 sub { my($cdr, $field) = @_; }, #Billing type
721 sub { my($cdr, $field) = @_; }, #Billing reference
726 sub { my($cdr, $date) = @_;
727 $date =~ /^(\d{1,2})\/(\d{1,2})\/(\d\d(\d\d)?)$/
728 or die "unparsable date: $date"; #maybe we shouldn't die...
729 #$cdr->startdate( timelocal(0, 0, 0 ,$2, $1-1, $3) );
730 ($tmp_mday, $tmp_mon, $tmp_year) = ( $2, $1-1, $3 );
734 sub { my($cdr, $time) = @_;
735 #my($sec, $min, $hour, $mday, $mon, $year)= localtime($cdr->startdate);
736 $time =~ /^(\d{1,2}):(\d{1,2}):(\d{1,2})$/
737 or die "unparsable time: $time"; #maybe we shouldn't die...
738 #$cdr->startdate( timelocal($3, $2, $1 ,$mday, $mon, $year) );
740 timelocal($3, $2, $1 ,$tmp_mday, $tmp_mon, $tmp_year)
751 sub { my($cdr, $min) = @_;
752 my $sec = sprintf('%.0f', $min * 60 );
753 $cdr->billsec( $sec );
754 $cdr->duration( $sec );
760 my %import_header = (
763 'voxlinesystems' => 2, #XXX vendor name
766 =item batch_import HASHREF
768 Imports CDR records. Available options are:
783 my $fh = $param->{filehandle};
784 my $format = $param->{format};
786 return "Unknown format $format" unless exists $import_formats{$format};
788 eval "use Text::CSV_XS;";
791 my $csv = new Text::CSV_XS;
796 local $SIG{HUP} = 'IGNORE';
797 local $SIG{INT} = 'IGNORE';
798 local $SIG{QUIT} = 'IGNORE';
799 local $SIG{TERM} = 'IGNORE';
800 local $SIG{TSTP} = 'IGNORE';
801 local $SIG{PIPE} = 'IGNORE';
803 my $oldAutoCommit = $FS::UID::AutoCommit;
804 local $FS::UID::AutoCommit = 0;
808 exists($import_header{$format}) ? $import_header{$format} : 0;
811 while ( defined($line=<$fh>) ) {
813 next if $header_lines-- > 0; #&& $line =~ /^[\w, "]+$/
815 $csv->parse($line) or do {
816 $dbh->rollback if $oldAutoCommit;
817 return "can't parse: ". $csv->error_input();
820 my @columns = $csv->fields();
821 #warn join('-',@columns);
823 if ( $format eq 'simple' ) {
824 @columns = map { s/^ +//; $_; } @columns;
831 my $field_or_sub = $_;
832 if ( ref($field_or_sub) ) {
833 push @later, $field_or_sub, shift(@columns);
836 ( $field_or_sub => shift @columns );
840 @{ $import_formats{$format} }
843 my $cdr = new FS::cdr ( \%cdr );
845 while ( scalar(@later) ) {
846 my $sub = shift @later;
847 my $data = shift @later;
848 &{$sub}($cdr, $data); # $cdr->&{$sub}($data);
851 if ( $format eq 'taqua' ) {
852 if ( $cdr->enddate && $cdr->startdate ) { #a bit more?
853 $cdr->duration( $cdr->enddate - $cdr->startdate );
855 if ( $cdr->enddate && $cdr->answerdate ) { #a bit more?
856 $cdr->billsec( $cdr->enddate - $cdr->answerdate );
860 my $error = $cdr->insert;
862 $dbh->rollback if $oldAutoCommit;
872 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
874 #might want to disable this if we skip records for any reason...
875 return "Empty file!" unless $imported;
887 L<FS::Record>, schema.html from the base documentation.