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, ... ]
397 'voxlinesystems' => { 'name' => 'VoxLineSystems',
399 "Date,Time,Name,Destination,Duration,Price",
401 'voxlinesystems2' => { 'name' => 'VoxLineSystems with source',
403 "Date,Time,Name,Destination,Called From,Duration,Price",
407 my %export_formats = (
409 'carriername', #CARRIER
410 sub { shift->_convergent_format('src') }, #SERVICE_NUMBER
411 sub { shift->_convergent_format('charged_party') }, #CHARGED_NUMBER
412 sub { time2str('%Y-%m-%d', shift->calldate_unix ) }, #DATE
413 sub { time2str('%T', shift->calldate_unix ) }, #TIME
414 'billsec', #'duration', #DURATION
415 sub { shift->_convergent_format('dst') }, #NUMBER_DIALED
416 '', #XXX add (from prefixes in most recent email) #FROM_DESC
417 '', #XXX add (from prefixes in most recent email) #TO_DESC
418 'calltypename', #CLASS_CODE
419 'rated_price', #PRICE
420 sub { shift->rated_price ? 'Y' : 'N' }, #RATED
423 'voxlinesystems' => [
424 sub { time2str('%D', shift->calldate_unix ) }, #DATE
425 sub { time2str('%T', shift->calldate_unix ) }, #TIME
427 'dst', #NUMBER_DIALED
428 sub { sprintf('%.2fm', shift->billsec / 60 ) }, #DURATION
429 sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
431 'voxlinesystems2' => [
432 sub { time2str('%D', shift->calldate_unix ) }, #DATE
433 sub { time2str('%T', shift->calldate_unix ) }, #TIME
435 'dst', #NUMBER_DIALED
437 sub { sprintf('%.2fm', shift->billsec / 60 ) }, #DURATION
438 sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
443 my( $self, %opt ) = @_;
445 my $format = $opt{'format'}; # 'convergent';
446 return "Unknown format $format" unless exists $export_formats{$format};
448 eval "use Text::CSV_XS;";
450 my $csv = new Text::CSV_XS;
454 ref($_) ? &{$_}($self) : $self->$_();
456 @{ $export_formats{$format} };
458 my $status = $csv->combine(@columns);
459 die "FS::CDR: error combining ". $csv->error_input(). "into downstream CSV"
472 =item invoice_formats
474 Returns an ordered list of key value pairs containing invoice format names
475 as keys (for use with part_pkg::voip_cdr) and "pretty" format names as values.
479 sub invoice_formats {
480 map { ($_ => $export_names{$_}->{'name'}) }
481 grep { $export_names{$_}->{'invoice_header'} }
485 =item invoice_header FORMAT
487 Returns a scalar containing the CSV column header for invoice format FORMAT.
493 $export_names{$format}->{'invoice_header'};
498 Returns an ordered list of key value pairs containing import format names
499 as keys (for use with batch_import) and "pretty" format names as values.
505 'asterisk' => 'Asterisk',
507 'unitel' => 'Unitel/RSLCOM',
508 'voxlinesystems' => 'VoxLineSystems', #XXX? get the actual vendor name
509 'simple' => 'Simple',
513 my($tmp_mday, $tmp_mon, $tmp_year);
515 sub _cdr_date_parser_maker {
518 my( $cdr, $date ) = @_;
519 #$cdr->$field( _cdr_date_parse($date) );
520 eval { $cdr->$field( _cdr_date_parse($date) ); };
521 die "error parsing date for $field from $date: $@\n" if $@;
525 sub _cdr_date_parse {
528 return '' unless length($date); #that's okay, it becomes NULL
530 #$date =~ /^\s*(\d{4})[\-\/]\(\d{1,2})[\-\/](\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s*$/
531 $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|$)/
532 or die "unparsable date: $date"; #maybe we shouldn't die...
533 my($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
535 return '' if $year == 1900 && $mon == 1 && $day == 1
536 && $hour == 0 && $min == 0 && $sec == 0;
538 timelocal($sec, $min, $hour, $day, $mon-1, $year);
541 #taqua #2007-10-31 08:57:24.113000000
543 #http://www.the-asterisk-book.com/unstable/funktionen-cdr.html
546 OMIT => 1, #asterisk 1.4+
547 IGNORE => 1, #asterisk 1.2
548 BILLING => 2, #asterisk 1.4+
549 BILL => 2, #asterisk 1.2
554 my %import_formats = (
565 _cdr_date_parser_maker('startdate'),
566 _cdr_date_parser_maker('answerdate'),
567 _cdr_date_parser_maker('enddate'),
571 sub { my($cdr, $amaflags) = @_; $cdr->amaflags($amaflags{$amaflags}); },
575 'taqua' => [ #some of these are kind arbitrary...
577 sub { my($cdr, $field) = @_; }, #XXX interesting RecordType
578 # easy to fix: Can't find cdr.cdrtypenum 1 in cdr_type.cdrtypenum
580 sub { my($cdr, $field) = @_; }, #all10#RecordVersion
581 sub { my($cdr, $field) = @_; }, #OrigShelfNumber
582 sub { my($cdr, $field) = @_; }, #OrigCardNumber
583 sub { my($cdr, $field) = @_; }, #OrigCircuit
584 sub { my($cdr, $field) = @_; }, #OrigCircuitType
585 'uniqueid', #SequenceNumber
586 'accountcode', #SessionNumber
587 'src', #CallingPartyNumber
588 'dst', #CalledPartyNumber
589 _cdr_date_parser_maker('startdate'), #CallArrivalTime
590 _cdr_date_parser_maker('enddate'), #CallCompletionTime
593 #sub { my($cdr, $d ) = @_; $cdr->disposition( $disposition{$d}): },
606 _cdr_date_parser_maker('answerdate'), #DispositionTime
607 sub { my($cdr, $field) = @_; }, #TCAP
608 sub { my($cdr, $field) = @_; }, #OutboundCarrierConnectTime
609 sub { my($cdr, $field) = @_; }, #OutboundCarrierDisconnectTime
612 #it appears channels are actually part of trunk groups, but this data
613 #is interesting and we need a source and destination place to put it
614 'dstchannel', #TermTrunkGroup
617 sub { my($cdr, $field) = @_; }, #TermShelfNumber
618 sub { my($cdr, $field) = @_; }, #TermCardNumber
619 sub { my($cdr, $field) = @_; }, #TermCircuit
620 sub { my($cdr, $field) = @_; }, #TermCircuitType
621 sub { my($cdr, $field) = @_; }, #OutboundCarrierId
622 'charged_party', #BillingNumber
623 sub { my($cdr, $field) = @_; }, #SubscriberNumber
624 'lastapp', #ServiceName
625 sub { my($cdr, $field) = @_; }, #some weirdness #ChargeTime
626 'lastdata', #ServiceInformation
627 sub { my($cdr, $field) = @_; }, #FacilityInfo
628 sub { my($cdr, $field) = @_; }, #all 1900-01-01 0#CallTraceTime
629 sub { my($cdr, $field) = @_; }, #all-1#UniqueIndicator
630 sub { my($cdr, $field) = @_; }, #all-1#PresentationIndicator
631 sub { my($cdr, $field) = @_; }, #empty#Pin
632 sub { my($cdr, $field) = @_; }, #CallType
633 sub { my($cdr, $field) = @_; }, #Balt/empty #OrigRateCenter
634 sub { my($cdr, $field) = @_; }, #Balt/empty #TermRateCenter
637 #it appears channels are actually part of trunk groups, but this data
638 #is interesting and we need a source and destination place to put it
639 'channel', #OrigTrunkGroup
641 'userfield', #empty#UserDefined
642 sub { my($cdr, $field) = @_; }, #empty#PseudoDestinationNumber
643 sub { my($cdr, $field) = @_; }, #all-1#PseudoCarrierCode
644 sub { my($cdr, $field) = @_; }, #empty#PseudoANI
645 sub { my($cdr, $field) = @_; }, #all-1#PseudoFacilityInfo
646 sub { my($cdr, $field) = @_; }, #OrigDialedDigits
647 sub { my($cdr, $field) = @_; }, #all-1#OrigOutboundCarrier
648 sub { my($cdr, $field) = @_; }, #IncomingCarrierID
649 'dcontext', #JurisdictionInfo
650 sub { my($cdr, $field) = @_; }, #OrigDestDigits
651 sub { my($cdr, $field) = @_; }, #huh?#InsertTime
652 sub { my($cdr, $field) = @_; }, #key
653 sub { my($cdr, $field) = @_; }, #empty#AMALineNumber
654 sub { my($cdr, $field) = @_; }, #empty#AMAslpID
655 sub { my($cdr, $field) = @_; }, #empty#AMADigitsDialedWC
656 sub { my($cdr, $field) = @_; }, #OpxOffHook
657 sub { my($cdr, $field) = @_; }, #OpxOnHook
659 #acctid - primary key
660 #AUTO #calldate - Call timestamp (SQL timestamp)
661 #clid - Caller*ID with text
662 #XXX src - Caller*ID number / Source number
663 #XXX dst - Destination extension
664 #dcontext - Destination context
665 #channel - Channel used
666 #dstchannel - Destination channel if appropriate
667 #lastapp - Last application if appropriate
668 #lastdata - Last application data
669 #startdate - Start of call (UNIX-style integer timestamp)
670 #answerdate - Answer time of call (UNIX-style integer timestamp)
671 #enddate - End time of call (UNIX-style integer timestamp)
672 #HACK#duration - Total time in system, in seconds
673 #HACK#XXX billsec - Total time call is up, in seconds
674 #disposition - What happened to the call: ANSWERED, NO ANSWER, BUSY
675 #INT amaflags - What flags to use: BILL, IGNORE etc, specified on a per channel basis like accountcode.
676 #accountcode - CDR account number to use: account
678 #uniqueid - Unique channel identifier (Unitel/RSLCOM Event ID)
679 #userfield - CDR user-defined field
681 #X cdrtypenum - CDR type - see FS::cdr_type (Usage = 1, S&E = 7, OC&C = 8)
682 #XXX charged_party - Service number to be billed
683 #upstream_currency - Wholesale currency from upstream
684 #X upstream_price - Wholesale price from upstream
685 #upstream_rateplanid - Upstream rate plan ID
686 #rated_price - Rated (or re-rated) price
687 #distance - km (need units field?)
688 #islocal - Local - 1, Non Local = 0
689 #calltypenum - Type of call - see FS::cdr_calltype
690 #X description - Description (cdr_type 7&8 only) (used for cust_bill_pkg.itemdesc)
691 #quantity - Number of items (cdr_type 7&8 only)
692 #carrierid - Upstream Carrier ID (see FS::cdr_carrier)
693 #upstream_rateid - Upstream Rate ID
695 #svcnum - Link to customer service (see FS::cust_svc)
696 #freesidestatus - NULL, done (or something)
703 'calldate', # may need massaging? huh maybe not...
704 #'billsec', #XXX duration and billsec?
705 sub { $_[0]->billsec( $_[1] );
706 $_[0]->duration( $_[1] );
709 'dst', # XXX needs to have "+61" prepended unless /^\+/ ???
713 'upstream_rateplanid',
717 'startdate', #XXX needs massaging
724 'voxlinesystems' => [ #XXX get the actual vendor name
725 'disposition', #Status
726 'startdate', #Start (what do you know, a timestamp!
727 sub { my($cdr, $field) = @_; }, #Start date
728 sub { my($cdr, $field) = @_; }, #Start time
729 'enddate', #End (also a timestamp!)
730 sub { my($cdr, $field) = @_; }, #End date
731 sub { my($cdr, $field) = @_; }, #End time
732 'accountcode', #Calling customer XXX map to agent_custid??
733 sub { my($cdr, $field) = @_; }, #Calling type
734 sub { shift->src('30000'); }, #XXX FAKE XXX 'src', #Calling number
735 'userfield', #Calling name #?
736 sub { my($cdr, $field) = @_; }, #Called type
737 'dst', #Called number
738 sub { my($cdr, $field) = @_; }, #Destination customer
739 sub { my($cdr, $field) = @_; }, #Destination type
740 sub { my($cdr, $field) = @_; }, #Destination Number
741 sub { my($cdr, $field) = @_; }, #Inbound calling type
742 sub { my($cdr, $field) = @_; }, #Inbound calling number
743 sub { my($cdr, $field) = @_; }, #Inbound called type
744 sub { my($cdr, $field) = @_; }, #Inbound called number
745 sub { my($cdr, $field) = @_; }, #Inbound destination type
746 sub { my($cdr, $field) = @_; }, #Inbound destination number
747 sub { my($cdr, $field) = @_; }, #Outbound calling type
748 sub { my($cdr, $field) = @_; }, #Outbound calling number
749 sub { my($cdr, $field) = @_; }, #Outbound called type
750 sub { my($cdr, $field) = @_; }, #Outbound called number
751 sub { my($cdr, $field) = @_; }, #Outbound destination type
752 sub { my($cdr, $field) = @_; }, #Outbound destination number
753 sub { my($cdr, $field) = @_; }, #Internal calling type
754 sub { my($cdr, $field) = @_; }, #Internal calling number
755 sub { my($cdr, $field) = @_; }, #Internal called type
756 sub { my($cdr, $field) = @_; }, #Internal called number
757 sub { my($cdr, $field) = @_; }, #Internal destination type
758 sub { my($cdr, $field) = @_; }, #Internal destination number
759 'duration', #Total seconds
760 sub { my($cdr, $field) = @_; }, #Ring seconds
761 'billsec', #Billable seconds
762 'upstream_price', #Cost
763 sub { my($cdr, $field) = @_; }, #Billing customer
764 sub { my($cdr, $field) = @_; }, #Billing customer name
765 sub { my($cdr, $field) = @_; }, #Billing type
766 sub { my($cdr, $field) = @_; }, #Billing reference
771 sub { my($cdr, $date) = @_;
772 $date =~ /^(\d{1,2})\/(\d{1,2})\/(\d\d(\d\d)?)$/
773 or die "unparsable date: $date"; #maybe we shouldn't die...
774 #$cdr->startdate( timelocal(0, 0, 0 ,$2, $1-1, $3) );
775 ($tmp_mday, $tmp_mon, $tmp_year) = ( $2, $1-1, $3 );
779 sub { my($cdr, $time) = @_;
780 #my($sec, $min, $hour, $mday, $mon, $year)= localtime($cdr->startdate);
781 $time =~ /^(\d{1,2}):(\d{1,2}):(\d{1,2})$/
782 or die "unparsable time: $time"; #maybe we shouldn't die...
783 #$cdr->startdate( timelocal($3, $2, $1 ,$mday, $mon, $year) );
785 timelocal($3, $2, $1 ,$tmp_mday, $tmp_mon, $tmp_year)
796 sub { my($cdr, $min) = @_;
797 my $sec = sprintf('%.0f', $min * 60 );
798 $cdr->billsec( $sec );
799 $cdr->duration( $sec );
805 my %import_header = (
808 'voxlinesystems' => 2, #XXX vendor name
811 =item batch_import HASHREF
813 Imports CDR records. Available options are:
828 my $fh = $param->{filehandle};
829 my $format = $param->{format};
831 return "Unknown format $format" unless exists $import_formats{$format};
833 eval "use Text::CSV_XS;";
836 my $csv = new Text::CSV_XS;
841 local $SIG{HUP} = 'IGNORE';
842 local $SIG{INT} = 'IGNORE';
843 local $SIG{QUIT} = 'IGNORE';
844 local $SIG{TERM} = 'IGNORE';
845 local $SIG{TSTP} = 'IGNORE';
846 local $SIG{PIPE} = 'IGNORE';
848 my $oldAutoCommit = $FS::UID::AutoCommit;
849 local $FS::UID::AutoCommit = 0;
853 exists($import_header{$format}) ? $import_header{$format} : 0;
856 while ( defined($line=<$fh>) ) {
858 next if $header_lines-- > 0; #&& $line =~ /^[\w, "]+$/
860 $csv->parse($line) or do {
861 $dbh->rollback if $oldAutoCommit;
862 return "can't parse: ". $csv->error_input();
865 my @columns = $csv->fields();
866 #warn join('-',@columns);
868 if ( $format eq 'simple' ) {
869 @columns = map { s/^ +//; $_; } @columns;
876 my $field_or_sub = $_;
877 if ( ref($field_or_sub) ) {
878 push @later, $field_or_sub, shift(@columns);
881 ( $field_or_sub => shift @columns );
885 @{ $import_formats{$format} }
888 my $cdr = new FS::cdr ( \%cdr );
890 while ( scalar(@later) ) {
891 my $sub = shift @later;
892 my $data = shift @later;
893 &{$sub}($cdr, $data); # $cdr->&{$sub}($data);
896 if ( $format eq 'taqua' ) {
897 if ( $cdr->enddate && $cdr->startdate ) { #a bit more?
898 $cdr->duration( $cdr->enddate - $cdr->startdate );
900 if ( $cdr->enddate && $cdr->answerdate ) { #a bit more?
901 $cdr->billsec( $cdr->enddate - $cdr->answerdate );
905 my $error = $cdr->insert;
907 $dbh->rollback if $oldAutoCommit;
917 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
919 #might want to disable this if we skip records for any reason...
920 return "Empty file!" unless $imported;
932 L<FS::Record>, schema.html from the base documentation.