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,Called From,Destination,Duration,Price",
404 "Date,Time,Called From,Destination,Duration,Price",
408 my %export_formats = (
410 'carriername', #CARRIER
411 sub { shift->_convergent_format('src') }, #SERVICE_NUMBER
412 sub { shift->_convergent_format('charged_party') }, #CHARGED_NUMBER
413 sub { time2str('%Y-%m-%d', shift->calldate_unix ) }, #DATE
414 sub { time2str('%T', shift->calldate_unix ) }, #TIME
415 'billsec', #'duration', #DURATION
416 sub { shift->_convergent_format('dst') }, #NUMBER_DIALED
417 '', #XXX add (from prefixes in most recent email) #FROM_DESC
418 '', #XXX add (from prefixes in most recent email) #TO_DESC
419 'calltypename', #CLASS_CODE
420 'rated_price', #PRICE
421 sub { shift->rated_price ? 'Y' : 'N' }, #RATED
424 'voxlinesystems' => [
425 sub { time2str('%D', shift->calldate_unix ) }, #DATE
426 sub { time2str('%r', shift->calldate_unix ) }, #TIME
428 'dst', #NUMBER_DIALED
429 sub { sprintf('%.2fm', shift->billsec / 60 ) }, #DURATION
430 sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
432 'voxlinesystems2' => [
433 sub { time2str('%D', shift->calldate_unix ) }, #DATE
434 sub { time2str('%r', shift->calldate_unix ) }, #TIME
436 'dst', #NUMBER_DIALED
438 sub { sprintf('%.2fm', shift->billsec / 60 ) }, #DURATION
439 sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
444 my( $self, %opt ) = @_;
446 my $format = $opt{'format'}; # 'convergent';
447 return "Unknown format $format" unless exists $export_formats{$format};
449 eval "use Text::CSV_XS;";
451 my $csv = new Text::CSV_XS;
455 ref($_) ? &{$_}($self) : $self->$_();
457 @{ $export_formats{$format} };
459 my $status = $csv->combine(@columns);
460 die "FS::CDR: error combining ". $csv->error_input(). "into downstream CSV"
473 =item invoice_formats
475 Returns an ordered list of key value pairs containing invoice format names
476 as keys (for use with part_pkg::voip_cdr) and "pretty" format names as values.
480 sub invoice_formats {
481 map { ($_ => $export_names{$_}->{'name'}) }
482 grep { $export_names{$_}->{'invoice_header'} }
486 =item invoice_header FORMAT
488 Returns a scalar containing the CSV column header for invoice format FORMAT.
494 $export_names{$format}->{'invoice_header'};
499 Returns an ordered list of key value pairs containing import format names
500 as keys (for use with batch_import) and "pretty" format names as values.
506 'asterisk' => 'Asterisk',
508 'unitel' => 'Unitel/RSLCOM',
509 'voxlinesystems' => 'VoxLineSystems', #XXX? get the actual vendor name
510 'simple' => 'Simple',
514 my($tmp_mday, $tmp_mon, $tmp_year);
516 sub _cdr_date_parser_maker {
519 my( $cdr, $date ) = @_;
520 #$cdr->$field( _cdr_date_parse($date) );
521 eval { $cdr->$field( _cdr_date_parse($date) ); };
522 die "error parsing date for $field from $date: $@\n" if $@;
526 sub _cdr_date_parse {
529 return '' unless length($date); #that's okay, it becomes NULL
531 #$date =~ /^\s*(\d{4})[\-\/]\(\d{1,2})[\-\/](\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s*$/
532 $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|$)/
533 or die "unparsable date: $date"; #maybe we shouldn't die...
534 my($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
536 return '' if $year == 1900 && $mon == 1 && $day == 1
537 && $hour == 0 && $min == 0 && $sec == 0;
539 timelocal($sec, $min, $hour, $day, $mon-1, $year);
542 #taqua #2007-10-31 08:57:24.113000000
544 #http://www.the-asterisk-book.com/unstable/funktionen-cdr.html
547 OMIT => 1, #asterisk 1.4+
548 IGNORE => 1, #asterisk 1.2
549 BILLING => 2, #asterisk 1.4+
550 BILL => 2, #asterisk 1.2
555 my %import_formats = (
566 _cdr_date_parser_maker('startdate'),
567 _cdr_date_parser_maker('answerdate'),
568 _cdr_date_parser_maker('enddate'),
572 sub { my($cdr, $amaflags) = @_; $cdr->amaflags($amaflags{$amaflags}); },
576 'taqua' => [ #some of these are kind arbitrary...
578 sub { my($cdr, $field) = @_; }, #XXX interesting RecordType
579 # easy to fix: Can't find cdr.cdrtypenum 1 in cdr_type.cdrtypenum
581 sub { my($cdr, $field) = @_; }, #all10#RecordVersion
582 sub { my($cdr, $field) = @_; }, #OrigShelfNumber
583 sub { my($cdr, $field) = @_; }, #OrigCardNumber
584 sub { my($cdr, $field) = @_; }, #OrigCircuit
585 sub { my($cdr, $field) = @_; }, #OrigCircuitType
586 'uniqueid', #SequenceNumber
587 'accountcode', #SessionNumber
588 'src', #CallingPartyNumber
589 'dst', #CalledPartyNumber
590 _cdr_date_parser_maker('startdate'), #CallArrivalTime
591 _cdr_date_parser_maker('enddate'), #CallCompletionTime
594 #sub { my($cdr, $d ) = @_; $cdr->disposition( $disposition{$d}): },
607 _cdr_date_parser_maker('answerdate'), #DispositionTime
608 sub { my($cdr, $field) = @_; }, #TCAP
609 sub { my($cdr, $field) = @_; }, #OutboundCarrierConnectTime
610 sub { my($cdr, $field) = @_; }, #OutboundCarrierDisconnectTime
613 #it appears channels are actually part of trunk groups, but this data
614 #is interesting and we need a source and destination place to put it
615 'dstchannel', #TermTrunkGroup
618 sub { my($cdr, $field) = @_; }, #TermShelfNumber
619 sub { my($cdr, $field) = @_; }, #TermCardNumber
620 sub { my($cdr, $field) = @_; }, #TermCircuit
621 sub { my($cdr, $field) = @_; }, #TermCircuitType
622 sub { my($cdr, $field) = @_; }, #OutboundCarrierId
623 'charged_party', #BillingNumber
624 sub { my($cdr, $field) = @_; }, #SubscriberNumber
625 'lastapp', #ServiceName
626 sub { my($cdr, $field) = @_; }, #some weirdness #ChargeTime
627 'lastdata', #ServiceInformation
628 sub { my($cdr, $field) = @_; }, #FacilityInfo
629 sub { my($cdr, $field) = @_; }, #all 1900-01-01 0#CallTraceTime
630 sub { my($cdr, $field) = @_; }, #all-1#UniqueIndicator
631 sub { my($cdr, $field) = @_; }, #all-1#PresentationIndicator
632 sub { my($cdr, $field) = @_; }, #empty#Pin
633 sub { my($cdr, $field) = @_; }, #CallType
634 sub { my($cdr, $field) = @_; }, #Balt/empty #OrigRateCenter
635 sub { my($cdr, $field) = @_; }, #Balt/empty #TermRateCenter
638 #it appears channels are actually part of trunk groups, but this data
639 #is interesting and we need a source and destination place to put it
640 'channel', #OrigTrunkGroup
642 'userfield', #empty#UserDefined
643 sub { my($cdr, $field) = @_; }, #empty#PseudoDestinationNumber
644 sub { my($cdr, $field) = @_; }, #all-1#PseudoCarrierCode
645 sub { my($cdr, $field) = @_; }, #empty#PseudoANI
646 sub { my($cdr, $field) = @_; }, #all-1#PseudoFacilityInfo
647 sub { my($cdr, $field) = @_; }, #OrigDialedDigits
648 sub { my($cdr, $field) = @_; }, #all-1#OrigOutboundCarrier
649 sub { my($cdr, $field) = @_; }, #IncomingCarrierID
650 'dcontext', #JurisdictionInfo
651 sub { my($cdr, $field) = @_; }, #OrigDestDigits
652 sub { my($cdr, $field) = @_; }, #huh?#InsertTime
653 sub { my($cdr, $field) = @_; }, #key
654 sub { my($cdr, $field) = @_; }, #empty#AMALineNumber
655 sub { my($cdr, $field) = @_; }, #empty#AMAslpID
656 sub { my($cdr, $field) = @_; }, #empty#AMADigitsDialedWC
657 sub { my($cdr, $field) = @_; }, #OpxOffHook
658 sub { my($cdr, $field) = @_; }, #OpxOnHook
660 #acctid - primary key
661 #AUTO #calldate - Call timestamp (SQL timestamp)
662 #clid - Caller*ID with text
663 #XXX src - Caller*ID number / Source number
664 #XXX dst - Destination extension
665 #dcontext - Destination context
666 #channel - Channel used
667 #dstchannel - Destination channel if appropriate
668 #lastapp - Last application if appropriate
669 #lastdata - Last application data
670 #startdate - Start of call (UNIX-style integer timestamp)
671 #answerdate - Answer time of call (UNIX-style integer timestamp)
672 #enddate - End time of call (UNIX-style integer timestamp)
673 #HACK#duration - Total time in system, in seconds
674 #HACK#XXX billsec - Total time call is up, in seconds
675 #disposition - What happened to the call: ANSWERED, NO ANSWER, BUSY
676 #INT amaflags - What flags to use: BILL, IGNORE etc, specified on a per channel basis like accountcode.
677 #accountcode - CDR account number to use: account
679 #uniqueid - Unique channel identifier (Unitel/RSLCOM Event ID)
680 #userfield - CDR user-defined field
682 #X cdrtypenum - CDR type - see FS::cdr_type (Usage = 1, S&E = 7, OC&C = 8)
683 #XXX charged_party - Service number to be billed
684 #upstream_currency - Wholesale currency from upstream
685 #X upstream_price - Wholesale price from upstream
686 #upstream_rateplanid - Upstream rate plan ID
687 #rated_price - Rated (or re-rated) price
688 #distance - km (need units field?)
689 #islocal - Local - 1, Non Local = 0
690 #calltypenum - Type of call - see FS::cdr_calltype
691 #X description - Description (cdr_type 7&8 only) (used for cust_bill_pkg.itemdesc)
692 #quantity - Number of items (cdr_type 7&8 only)
693 #carrierid - Upstream Carrier ID (see FS::cdr_carrier)
694 #upstream_rateid - Upstream Rate ID
696 #svcnum - Link to customer service (see FS::cust_svc)
697 #freesidestatus - NULL, done (or something)
704 'calldate', # may need massaging? huh maybe not...
705 #'billsec', #XXX duration and billsec?
706 sub { $_[0]->billsec( $_[1] );
707 $_[0]->duration( $_[1] );
710 'dst', # XXX needs to have "+61" prepended unless /^\+/ ???
714 'upstream_rateplanid',
718 'startdate', #XXX needs massaging
725 'voxlinesystems' => [ #XXX get the actual vendor name
726 'disposition', #Status
727 'startdate', #Start (what do you know, a timestamp!
728 sub { my($cdr, $field) = @_; }, #Start date
729 sub { my($cdr, $field) = @_; }, #Start time
730 'enddate', #End (also a timestamp!)
731 sub { my($cdr, $field) = @_; }, #End date
732 sub { my($cdr, $field) = @_; }, #End time
733 'accountcode', #Calling customer... map to agent_custid??
734 sub { my($cdr, $field) = @_; }, #Calling type
736 #sub { my($cdr, $field) = @_; }, #Calling number
737 'userfield', #Calling name #?
738 sub { my($cdr, $field) = @_; }, #Called type
739 'dst', #Called number
740 sub { my($cdr, $field) = @_; }, #Destination customer
741 sub { my($cdr, $field) = @_; }, #Destination type
742 sub { my($cdr, $field) = @_; }, #Destination Number
743 sub { my($cdr, $field) = @_; }, #Inbound calling type
744 sub { my($cdr, $field) = @_; }, #Inbound calling number
746 sub { my($cdr, $field) = @_; }, #Inbound called type
747 sub { my($cdr, $field) = @_; }, #Inbound called number
748 sub { my($cdr, $field) = @_; }, #Inbound destination type
749 sub { my($cdr, $field) = @_; }, #Inbound destination number
750 sub { my($cdr, $field) = @_; }, #Outbound calling type
751 sub { my($cdr, $field) = @_; }, #Outbound calling number
752 sub { my($cdr, $field) = @_; }, #Outbound called type
753 sub { my($cdr, $field) = @_; }, #Outbound called number
754 sub { my($cdr, $field) = @_; }, #Outbound destination type
755 sub { my($cdr, $field) = @_; }, #Outbound destination number
756 sub { my($cdr, $field) = @_; }, #Internal calling type
757 sub { my($cdr, $field) = @_; }, #Internal calling number
758 sub { my($cdr, $field) = @_; }, #Internal called type
759 sub { my($cdr, $field) = @_; }, #Internal called number
760 sub { my($cdr, $field) = @_; }, #Internal destination type
761 sub { my($cdr, $field) = @_; }, #Internal destination number
762 'duration', #Total seconds
763 sub { my($cdr, $field) = @_; }, #Ring seconds
764 'billsec', #Billable seconds
765 'upstream_price', #Cost
766 sub { my($cdr, $field) = @_; }, #Billing customer
767 sub { my($cdr, $field) = @_; }, #Billing customer name
768 sub { my($cdr, $field) = @_; }, #Billing type
769 sub { my($cdr, $field) = @_; }, #Billing reference
774 sub { my($cdr, $date) = @_;
775 $date =~ /^(\d{1,2})\/(\d{1,2})\/(\d\d(\d\d)?)$/
776 or die "unparsable date: $date"; #maybe we shouldn't die...
777 #$cdr->startdate( timelocal(0, 0, 0 ,$2, $1-1, $3) );
778 ($tmp_mday, $tmp_mon, $tmp_year) = ( $2, $1-1, $3 );
782 sub { my($cdr, $time) = @_;
783 #my($sec, $min, $hour, $mday, $mon, $year)= localtime($cdr->startdate);
784 $time =~ /^(\d{1,2}):(\d{1,2}):(\d{1,2})$/
785 or die "unparsable time: $time"; #maybe we shouldn't die...
786 #$cdr->startdate( timelocal($3, $2, $1 ,$mday, $mon, $year) );
788 timelocal($3, $2, $1 ,$tmp_mday, $tmp_mon, $tmp_year)
799 sub { my($cdr, $min) = @_;
800 my $sec = sprintf('%.0f', $min * 60 );
801 $cdr->billsec( $sec );
802 $cdr->duration( $sec );
808 my %import_header = (
811 'voxlinesystems' => 2, #XXX vendor name
814 =item batch_import HASHREF
816 Imports CDR records. Available options are:
831 my $fh = $param->{filehandle};
832 my $format = $param->{format};
834 return "Unknown format $format" unless exists $import_formats{$format};
836 eval "use Text::CSV_XS;";
839 my $csv = new Text::CSV_XS;
844 local $SIG{HUP} = 'IGNORE';
845 local $SIG{INT} = 'IGNORE';
846 local $SIG{QUIT} = 'IGNORE';
847 local $SIG{TERM} = 'IGNORE';
848 local $SIG{TSTP} = 'IGNORE';
849 local $SIG{PIPE} = 'IGNORE';
851 my $oldAutoCommit = $FS::UID::AutoCommit;
852 local $FS::UID::AutoCommit = 0;
856 exists($import_header{$format}) ? $import_header{$format} : 0;
859 while ( defined($line=<$fh>) ) {
861 next if $header_lines-- > 0; #&& $line =~ /^[\w, "]+$/
863 $csv->parse($line) or do {
864 $dbh->rollback if $oldAutoCommit;
865 return "can't parse: ". $csv->error_input();
868 my @columns = $csv->fields();
869 #warn join('-',@columns);
871 if ( $format eq 'simple' ) {
872 @columns = map { s/^ +//; $_; } @columns;
879 my $field_or_sub = $_;
880 if ( ref($field_or_sub) ) {
881 push @later, $field_or_sub, shift(@columns);
884 ( $field_or_sub => shift @columns );
888 @{ $import_formats{$format} }
891 my $cdr = new FS::cdr ( \%cdr );
893 while ( scalar(@later) ) {
894 my $sub = shift @later;
895 my $data = shift @later;
896 &{$sub}($cdr, $data); # $cdr->&{$sub}($data);
899 if ( $format eq 'taqua' ) {
900 if ( $cdr->enddate && $cdr->startdate ) { #a bit more?
901 $cdr->duration( $cdr->enddate - $cdr->startdate );
903 if ( $cdr->enddate && $cdr->answerdate ) { #a bit more?
904 $cdr->billsec( $cdr->enddate - $cdr->answerdate );
908 my $error = $cdr->insert;
910 $dbh->rollback if $oldAutoCommit;
920 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
922 #might want to disable this if we skip records for any reason...
923 return "Empty file!" unless $imported;
935 L<FS::Record>, schema.html from the base documentation.