rewrite CDRs for forwarded Asterisk calls to be billable, RT#3196
[freeside.git] / FS / FS / cdr.pm
index b4347da..8307b28 100644 (file)
@@ -1,11 +1,14 @@
 package FS::cdr;
 
 use strict;
-use vars qw( @ISA );
+use vars qw( @ISA @EXPORT_OK $DEBUG );
+use Exporter;
+use Tie::IxHash;
 use Date::Parse;
 use Date::Format;
 use Time::Local;
 use FS::UID qw( dbh );
+use FS::Conf;
 use FS::Record qw( qsearch qsearchs );
 use FS::cdr_type;
 use FS::cdr_calltype;
@@ -13,6 +16,9 @@ use FS::cdr_carrier;
 use FS::cdr_upstream_rate;
 
 @ISA = qw(FS::Record);
+@EXPORT_OK = qw( _cdr_date_parser_maker _cdr_min_parser_maker );
+
+$DEBUG = 0;
 
 =head1 NAME
 
@@ -124,6 +130,10 @@ following fields are currently supported:
 
 =item freesidestatus - NULL, done (or something)
 
+=item freesiderewritestatus - NULL, done (or something)
+
+=item cdrbatch
+
 =back
 
 =head1 METHODS
@@ -220,18 +230,40 @@ sub check {
 #    || $self->ut_numbern('upstream_rateid')
 #    || $self->ut_numbern('svcnum')
 #    || $self->ut_textn('freesidestatus')
+#    || $self->ut_textn('freesiderewritestatus')
 #  ;
 #  return $error if $error;
 
   $self->calldate( $self->startdate_sql )
     if !$self->calldate && $self->startdate;
 
+  #was just for $format eq 'taqua' but can't see the harm... add something to
+  #disable if it becomes a problem
+  if ( $self->duration eq '' && $self->enddate && $self->startdate ) {
+    $self->duration( $self->enddate - $self->startdate  );
+  }
+  if ( $self->billsec eq '' && $self->enddate && $self->answerdate ) {
+    $self->billsec(  $self->enddate - $self->answerdate );
+  } 
+
+  my $conf = new FS::Conf;
+
   unless ( $self->charged_party ) {
-    if ( $self->dst =~ /^(\+?1)?8[02-8]{2}/ ) {
-      $self->charged_party($self->dst);
+
+    if ( $conf->exists('cdr-charged_party-accountcode') && $self->accountcode ){
+
+      $self->charged_party( $self->accountcode );
+
     } else {
-      $self->charged_party($self->src);
+
+      if ( $self->dst =~ /^(\+?1)?8[02-8]{2}/ ) {
+        $self->charged_party($self->dst);
+      } else {
+        $self->charged_party($self->src);
+      }
+
     }
+
   }
 
   #check the foreign keys even?
@@ -239,14 +271,16 @@ sub check {
   my $error =
        $self->ut_numbern('acctid')
 
-    #Usage = 1, S&E = 7, OC&C = 8
-    || $self->ut_foreign_keyn('cdrtypenum',  'cdr_type',     'cdrtypenum' )
-
-    #the big list in appendix 2
-    || $self->ut_foreign_keyn('calltypenum', 'cdr_calltype', 'calltypenum' )
-
-    # Telstra =1, Optus = 2, RSL COM = 3
-    || $self->ut_foreign_keyn('carrierid', 'cdr_carrier', 'carrierid' )
+  #add a config option to turn these back on if someone needs 'em
+  #
+  #  #Usage = 1, S&E = 7, OC&C = 8
+  #  || $self->ut_foreign_keyn('cdrtypenum',  'cdr_type',     'cdrtypenum' )
+  #
+  #  #the big list in appendix 2
+  #  || $self->ut_foreign_keyn('calltypenum', 'cdr_calltype', 'calltypenum' )
+  #
+  #  # Telstra =1, Optus = 2, RSL COM = 3
+  #  || $self->ut_foreign_keyn('carrierid', 'cdr_carrier', 'carrierid' )
   ;
   return $error if $error;
 
@@ -392,6 +426,27 @@ sub _convergent_format {
 
 =cut
 
+my %export_names = (
+  'convergent'      => {},
+  'simple'  => {
+    'name'           => 'Simple',
+    'invoice_header' => "Date,Time,Name,Destination,Duration,Price",
+  },
+  'simple2' => {
+    'name'           => 'Simple with source',
+    'invoice_header' => "Date,Time,Called From,Destination,Duration,Price",
+                       #"Date,Time,Name,Called From,Destination,Duration,Price",
+  },
+  'default' => {
+    'name'           => 'Default',
+    'invoice_header' => 'Date,Time,Number,Destination,Duration,Price',
+  },
+  'source_default' => {
+    'name'           => 'Default with source',
+    'invoice_header' => 'Caller,Date,Time,Number,Destination,Duration,Price',
+  },
+);
+
 my %export_formats = (
   'convergent' => [
     'carriername', #CARRIER
@@ -408,7 +463,52 @@ my %export_formats = (
     sub { shift->rated_price ? 'Y' : 'N' }, #RATED
     '', #OTHER_INFO
   ],
+  'simple' => [
+    sub { time2str('%D', shift->calldate_unix ) },   #DATE
+    sub { time2str('%r', shift->calldate_unix ) },   #TIME
+    'userfield',                                     #USER
+    'dst',                                           #NUMBER_DIALED
+    sub { sprintf('%.2fm', shift->billsec / 60 ) },  #DURATION
+    #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
+    sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
+  ],
+  'simple2' => [
+    sub { time2str('%D', shift->calldate_unix ) },   #DATE
+    sub { time2str('%r', shift->calldate_unix ) },   #TIME
+    #'userfield',                                     #USER
+    'dst',                                           #NUMBER_DIALED
+    'src',                                           #called from
+    sub { sprintf('%.2fm', shift->billsec / 60 ) },  #DURATION
+    #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
+    sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
+  ],
+  'default' => [
+
+    #DATE
+    sub { time2str('%D', shift->calldate_unix ) },
+          # #time2str("%Y %b %d - %r", $cdr->calldate_unix ),
+
+    #TIME
+    sub { time2str('%r', shift->calldate_unix ) },
+          # time2str("%c", $cdr->calldate_unix),  #XXX this should probably be a config option dropdown so they can select US vs- rest of world dates or whatnot
+
+    #DEST ("Number")
+    sub { my($cdr, %opt) = @_; $opt{pretty_dst} || $cdr->dst; },
+
+    #REGIONNAME ("Destination")
+    sub { my($cdr, %opt) = @_; $opt{dst_regionname}; },
+
+    #DURATION
+    sub { my($cdr, %opt) = @_;
+          $opt{minutes}. ( $opt{granularity} ? 'm' : ' call' );
+        },
+
+    #PRICE
+    sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; },
+
+  ],
 );
+$export_formats{'source_default'} = [ 'src', @{ $export_formats{'default'} }, ];
 
 sub downstream_csv {
   my( $self, %opt ) = @_;
@@ -416,13 +516,17 @@ sub downstream_csv {
   my $format = $opt{'format'}; # 'convergent';
   return "Unknown format $format" unless exists $export_formats{$format};
 
+  #my $conf = new FS::Conf;
+  #$opt{'money_char'} ||= $conf->config('money_char') || '$';
+  $opt{'money_char'} ||= FS::Conf->new->config('money_char') || '$';
+
   eval "use Text::CSV_XS;";
   die $@ if $@;
   my $csv = new Text::CSV_XS;
 
   my @columns =
     map {
-          ref($_) ? &{$_}($self) : $self->$_();
+          ref($_) ? &{$_}($self, %opt) : $self->$_();
         }
     @{ $export_formats{$format} };
 
@@ -440,6 +544,30 @@ sub downstream_csv {
 
 =over 4
 
+=item invoice_formats
+
+Returns an ordered list of key value pairs containing invoice format names
+as keys (for use with part_pkg::voip_cdr) and "pretty" format names as values.
+
+=cut
+
+sub invoice_formats {
+  map { ($_ => $export_names{$_}->{'name'}) }
+    grep { $export_names{$_}->{'invoice_header'} }
+    keys %export_names;
+}
+
+=item invoice_header FORMAT
+
+Returns a scalar containing the CSV column header for invoice format FORMAT.
+
+=cut
+
+sub invoice_header {
+  my $format = shift;
+  $export_names{$format}->{'invoice_header'};
+}
+
 =item import_formats
 
 Returns an ordered list of key value pairs containing import format names
@@ -447,20 +575,72 @@ as keys (for use with batch_import) and "pretty" format names as values.
 
 =cut
 
+#false laziness w/part_pkg & part_export
+
+my %cdr_info;
+foreach my $INC ( @INC ) {
+  warn "globbing $INC/FS/cdr/*.pm\n" if $DEBUG;
+  foreach my $file ( glob("$INC/FS/cdr/*.pm") ) {
+    warn "attempting to load CDR format info from $file\n" if $DEBUG;
+    $file =~ /\/(\w+)\.pm$/ or do {
+      warn "unrecognized file in $INC/FS/cdr/: $file\n";
+      next;
+    };
+    my $mod = $1;
+    my $info = eval "use FS::cdr::$mod; ".
+                    "\\%FS::cdr::$mod\::info;";
+    if ( $@ ) {
+      die "error using FS::cdr::$mod (skipping): $@\n" if $@;
+      next;
+    }
+    unless ( keys %$info ) {
+      warn "no %info hash found in FS::cdr::$mod, skipping\n";
+      next;
+    }
+    warn "got CDR format info from FS::cdr::$mod: $info\n" if $DEBUG;
+    if ( exists($info->{'disabled'}) && $info->{'disabled'} ) {
+      warn "skipping disabled CDR format FS::cdr::$mod" if $DEBUG;
+      next;
+    }
+    $cdr_info{$mod} = $info;
+  }
+}
+
+tie my %import_formats, 'Tie::IxHash',
+  map  { $_ => $cdr_info{$_}->{'name'} }
+  sort { $cdr_info{$a}->{'weight'} <=> $cdr_info{$b}->{'weight'} }
+  grep { exists($cdr_info{$_}->{'import_fields'}) }
+  keys %cdr_info;
+
 sub import_formats {
-  'asterisk' => 'Asterisk',
-  'taqua'    => 'Taqua',
-  'unitel'   => 'Unitel/RSLCOM',
-  'simple'   => 'Simple',
+  %import_formats;
+}
+
+sub _cdr_min_parser_maker {
+  my $field = shift;
+  my @fields = ref($field) ? @$field : ($field);
+  @fields = qw( billsec duration ) unless scalar(@fields) && $fields[0];
+  return sub {
+    my( $cdr, $min ) = @_;
+    my $sec = eval { _cdr_min_parse($min) };
+    die "error parsing seconds for @fields from $min minutes: $@\n" if $@;
+    $cdr->$_($sec) foreach @fields;
+  };
 }
 
-my($tmp_mday, $tmp_mon, $tmp_year);
+sub _cdr_min_parse {
+  my $min = shift;
+  sprintf('%.0f', $min * 60 );
+}
 
 sub _cdr_date_parser_maker {
   my $field = shift;
+  my @fields = ref($field) ? @$field : ($field);
   return sub {
-    my( $cdr, $date ) = @_;
-    $cdr->$field( _cdr_date_parse($date) );
+    my( $cdr, $datestring ) = @_;
+    my $unixdate = eval { _cdr_date_parse($datestring) };
+    die "error parsing date for @fields from $datestring: $@\n" if $@;
+    $cdr->$_($unixdate) foreach @fields;
   };
 }
 
@@ -469,314 +649,105 @@ sub _cdr_date_parse {
 
   return '' unless length($date); #that's okay, it becomes NULL
 
+  my($year, $mon, $day, $hour, $min, $sec);
+
   #$date =~ /^\s*(\d{4})[\-\/]\(\d{1,2})[\-\/](\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s*$/
-  $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*$/
-    or die "unparsable date: $date"; #maybe we shouldn't die...
-  my($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
+  #taqua  #2007-10-31 08:57:24.113000000
+
+  if ( $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|$)/ ) {
+    ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
+  } elsif ( $date  =~ /^\s*(\d{1,2})\D(\d{1,2})\D(\d{4})\s+(\d{1,2})\D(\d{1,2})\D(\d{1,2})(\D|$)/ ) {
+    ($mon, $day, $year, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
+  } else {
+     die "unparsable date: $date"; #maybe we shouldn't die...
+  }
+
+  return '' if $year == 1900 && $mon == 1 && $day == 1
+            && $hour == 0    && $min == 0 && $sec == 0;
 
   timelocal($sec, $min, $hour, $day, $mon-1, $year);
 }
 
-#http://www.the-asterisk-book.com/unstable/funktionen-cdr.html
-my %amaflags = (
-  DEFAULT       => 0,
-  OMIT          => 1, #asterisk 1.4+
-  IGNORE        => 1, #asterisk 1.2
-  BILLING       => 2, #asterisk 1.4+
-  BILL          => 2, #asterisk 1.2
-  DOCUMENTATION => 3,
-  #? '' => 0,
-);
-
-my %import_formats = (
-  'asterisk' => [
-    'accountcode',
-    'src',
-    'dst',
-    'dcontext',
-    'clid',
-    'channel',
-    'dstchannel',
-    'lastapp',
-    'lastdata',
-    _cdr_date_parser_maker('startdate'),
-    _cdr_date_parser_maker('answerdate'),
-    _cdr_date_parser_maker('enddate'),
-    'duration',
-    'billsec',
-    'disposition',
-    sub { my($cdr, $amaflags) = @_; $cdr->amaflags($amaflags{$amaflags}); },
-    'uniqueid',
-    'userfield',
-  ],
-  'taqua' => [
-    sub { my($cdr, $field) = @_; }, #RecordType
-    sub { my($cdr, $field) = @_; },         #all10#RecordVersion
-    sub { my($cdr, $field) = @_; }, #OrigShelfNumber
-    sub { my($cdr, $field) = @_; }, #OrigCardNumber
-    sub { my($cdr, $field) = @_; }, #OrigCircuit
-    sub { my($cdr, $field) = @_; }, #OrigCircuitType
-    sub { my($cdr, $field) = @_; }, #SequenceNumber
-    sub { my($cdr, $field) = @_; }, #SessionNumber
-    sub { my($cdr, $field) = @_; }, #CallingPartyNumber
-    sub { my($cdr, $field) = @_; }, #CalledPartyNumber
-    sub { my($cdr, $field) = @_; }, #CallArrivalTime
-    sub { my($cdr, $field) = @_; }, #CallCompletionTime
-    sub { my($cdr, $field) = @_; }, #Disposition
-    sub { my($cdr, $field) = @_; }, #DispositionTime
-    sub { my($cdr, $field) = @_; }, #TCAP
-    sub { my($cdr, $field) = @_; }, #OutboundCarrierConnectTime
-    sub { my($cdr, $field) = @_; }, #OutboundCarrierDisconnectTime
-    sub { my($cdr, $field) = @_; }, #TermTrunkGroup
-    sub { my($cdr, $field) = @_; }, #TermShelfNumber
-    sub { my($cdr, $field) = @_; }, #TermCardNumber
-    sub { my($cdr, $field) = @_; }, #TermCircuit
-    sub { my($cdr, $field) = @_; }, #TermCircuitType
-    sub { my($cdr, $field) = @_; }, #OutboundCarrierId
-    sub { my($cdr, $field) = @_; }, #BillingNumber
-    sub { my($cdr, $field) = @_; }, #SubscriberNumber
-    sub { my($cdr, $field) = @_; }, #ServiceName
-    sub { my($cdr, $field) = @_; }, #ChargeTime
-    sub { my($cdr, $field) = @_; }, #ServiceInformation
-    sub { my($cdr, $field) = @_; }, #FacilityInfo
-    sub { my($cdr, $field) = @_; }, #CallTraceTime
-    sub { my($cdr, $field) = @_; },         #all-1#UniqueIndicator
-    sub { my($cdr, $field) = @_; },         #all-1#PresentationIndicator
-    sub { my($cdr, $field) = @_; },         #empty#Pin
-    sub { my($cdr, $field) = @_; }, #CallType
-    sub { my($cdr, $field) = @_; }, #OrigRateCenter
-    sub { my($cdr, $field) = @_; }, #TermRateCenter
-    sub { my($cdr, $field) = @_; }, #OrigTrunkGroup
-    'userfield',                            #empty#UserDefined
-    sub { my($cdr, $field) = @_; },         #empty#PseudoDestinationNumber
-    sub { my($cdr, $field) = @_; },         #all-1#PseudoCarrierCode
-    sub { my($cdr, $field) = @_; },         #empty#PseudoANI
-    sub { my($cdr, $field) = @_; },         #all-1#PseudoFacilityInfo
-    sub { my($cdr, $field) = @_; }, #OrigDialedDigits
-    sub { my($cdr, $field) = @_; },         #all-1#OrigOutboundCarrier
-    sub { my($cdr, $field) = @_; }, #IncomingCarrierID
-    sub { my($cdr, $field) = @_; }, #JurisdictionInfo
-    sub { my($cdr, $field) = @_; }, #OrigDestDigits
-    sub { my($cdr, $field) = @_; }, #InsertTime
-    sub { my($cdr, $field) = @_; }, #key
-    sub { my($cdr, $field) = @_; },         #empty#AMALineNumber
-    sub { my($cdr, $field) = @_; },         #empty#AMAslpID
-    sub { my($cdr, $field) = @_; },         #empty#AMADigitsDialedWC
-    sub { my($cdr, $field) = @_; }, #OpxOffHook
-    sub { my($cdr, $field) = @_; }, #OpxOnHook
-
-#acctid - primary key
-#calldate - Call timestamp (SQL timestamp)
-#clid - Caller*ID with text
-#src - Caller*ID number / Source number
-#dst - Destination extension
-#dcontext - Destination context
-#channel - Channel used
-#dstchannel - Destination channel if appropriate
-#lastapp - Last application if appropriate
-#lastdata - Last application data
-#startdate - Start of call (UNIX-style integer timestamp)
-#answerdate - Answer time of call (UNIX-style integer timestamp)
-#enddate - End time of call (UNIX-style integer timestamp)
-#duration - Total time in system, in seconds
-#billsec - Total time call is up, in seconds
-#disposition - What happened to the call: ANSWERED, NO ANSWER, BUSY
-#amaflags - What flags to use: BILL, IGNORE etc, specified on a per
-#channel basis like accountcode.
-#accountcode - CDR account number to use: account
-#uniqueid - Unique channel identifier (Unitel/RSLCOM Event ID)
-        #userfield - CDR user-defined field
-#cdr_type - CDR type - see FS::cdr_type (Usage = 1, S&E = 7, OC&C = 8)
-#charged_party - Service number to be billed
-#upstream_currency - Wholesale currency from upstream
-#upstream_price - Wholesale price from upstream
-#upstream_rateplanid - Upstream rate plan ID
-#rated_price - Rated (or re-rated) price
-#distance - km (need units field?)
-#islocal - Local - 1, Non Local = 0
-#calltypenum - Type of call - see FS::cdr_calltype
-#description - Description (cdr_type 7&8 only) (used for
-#cust_bill_pkg.itemdesc)
-#quantity - Number of items (cdr_type 7&8 only)
-#carrierid - Upstream Carrier ID (see FS::cdr_carrier)
-#upstream_rateid - Upstream Rate ID
-        #svcnum - Link to customer service (see FS::cust_svc)
-        #freesidestatus - NULL, done (or something)
-
-  ],
-  'unitel' => [
-    'uniqueid',
-    #'cdr_type',
-    'cdrtypenum',
-    'calldate', # may need massaging?  huh maybe not...
-    #'billsec', #XXX duration and billsec?
-                sub { $_[0]->billsec(  $_[1] );
-                      $_[0]->duration( $_[1] );
-                    },
-    'src',
-    'dst', # XXX needs to have "+61" prepended unless /^\+/ ???
-    'charged_party',
-    'upstream_currency',
-    'upstream_price',
-    'upstream_rateplanid',
-    'distance',
-    'islocal',
-    'calltypenum',
-    'startdate',  #XXX needs massaging
-    'enddate',    #XXX same
-    'description',
-    'quantity',
-    'carrierid',
-    'upstream_rateid',
-  ],
-  'simple' => [
-
-    # Date
-    sub { my($cdr, $date) = @_;
-          $date =~ /^(\d{1,2})\/(\d{1,2})\/(\d\d(\d\d)?)$/
-            or die "unparsable date: $date"; #maybe we shouldn't die...
-          #$cdr->startdate( timelocal(0, 0, 0 ,$2, $1-1, $3) );
-          ($tmp_mday, $tmp_mon, $tmp_year) = ( $2, $1-1, $3 );
-        },
-
-    # Time
-    sub { my($cdr, $time) = @_;
-          #my($sec, $min, $hour, $mday, $mon, $year)= localtime($cdr->startdate);
-          $time =~ /^(\d{1,2}):(\d{1,2}):(\d{1,2})$/
-            or die "unparsable time: $time"; #maybe we shouldn't die...
-          #$cdr->startdate( timelocal($3, $2, $1 ,$mday, $mon, $year) );
-          $cdr->startdate(
-            timelocal($3, $2, $1 ,$tmp_mday, $tmp_mon, $tmp_year)
-          );
-        },
-
-    # Source_Number
-    'src',
-
-    # Terminating_Number
-    'dst',
-
-    # Duration
-    sub { my($cdr, $min) = @_;
-          my $sec = sprintf('%.0f', $min * 60 );
-          $cdr->billsec(  $sec );
-          $cdr->duration( $sec );
-        },
-
-  ],
-);
-
-my %import_header = (
-  'simple' => 1,
-  'taqua'  => 1,
-);
-
 =item batch_import HASHREF
 
 Imports CDR records.  Available options are:
 
 =over 4
 
-=item filehandle
+=item file
 
-=item format
-
-=back
-
-=cut
-
-sub batch_import {
-  my $param = shift;
+Filename
 
-  my $fh = $param->{filehandle};
-  my $format = $param->{format};
+=item format
 
-  return "Unknown format $format" unless exists $import_formats{$format};
+=item params
 
-  eval "use Text::CSV_XS;";
-  die $@ if $@;
+Hash reference of preset fields, typically cdrbatch
 
-  my $csv = new Text::CSV_XS;
+=item empty_ok
 
-  my $imported = 0;
-  #my $columns;
+Set true to prevent throwing an error on empty imports
 
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-  local $SIG{PIPE} = 'IGNORE';
+=back
 
-  my $oldAutoCommit = $FS::UID::AutoCommit;
-  local $FS::UID::AutoCommit = 0;
-  my $dbh = dbh;
+=cut
 
-  my $body = 0;
-  my $line;
-  while ( defined($line=<$fh>) ) {
+my %import_options = (
+  'table'   => 'cdr',
 
-    #skip header...
-    if ( ! $body++ && $import_header{'format'} && $line =~ /^[\w\, ]+$/ ) {
-      next;
-    }
+  'formats' => { map { $_ => $cdr_info{$_}->{'import_fields'}; }
+                     keys %cdr_info
+               },
 
-    $csv->parse($line) or do {
-      $dbh->rollback if $oldAutoCommit;
-      return "can't parse: ". $csv->error_input();
-    };
+                          #drop the || 'csv' to allow auto xls for csv types?
+  'format_types' => { map { $_ => ( lc($cdr_info{$_}->{'type'}) || 'csv' ); }
+                          keys %cdr_info
+                    },
 
-    my @columns = $csv->fields();
-    #warn join('-',@columns);
+  'format_headers' => { map { $_ => ( $cdr_info{$_}->{'header'} || 0 ); }
+                            keys %cdr_info
+                      },
 
-    if ( $format eq 'simple' ) {
-      @columns = map { s/^ +//; $_; } @columns;
-    }
+  'format_sep_chars' => { map { $_ => $cdr_info{$_}->{'sep_char'}; }
+                              keys %cdr_info
+                        },
 
-    my @later = ();
-    my %cdr =
-      map {
+  'format_fixedlength_formats' =>
+    { map { $_ => $cdr_info{$_}->{'fixedlength_format'}; }
+          keys %cdr_info
+    },
+);
 
-        my $field_or_sub = $_;
-        if ( ref($field_or_sub) ) {
-          push @later, $field_or_sub, shift(@columns);
-          ();
-        } else {
-          ( $field_or_sub => shift @columns );
-        }
+sub _import_options {
+  \%import_options;
+}
 
-      }
-      @{ $import_formats{$format} }
-    ;
+sub batch_import {
+  my $opt = shift;
 
-    my $cdr = new FS::cdr ( \%cdr );
+  my $iopt = _import_options;
+  $opt->{$_} = $iopt->{$_} foreach keys %$iopt;
 
-    while ( scalar(@later) ) {
-      my $sub = shift @later;
-      my $data = shift @later;
-      &{$sub}($cdr, $data);  # $cdr->&{$sub}($data); 
-    }
+  FS::Record::batch_import( $opt );
 
-    my $error = $cdr->insert;
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return $error;
+}
 
-      #or just skip?
-      #next;
-    }
+=item process_batch_import
 
-    $imported++;
-  }
+=cut
 
-  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+sub process_batch_import {
+  my $job = shift;
 
-  #might want to disable this if we skip records for any reason...
-  return "Empty file!" unless $imported;
+  my $opt = _import_options;
+  $opt->{'params'} = [ 'format', 'cdrbatch' ];
 
-  '';
+  FS::Record::process_batch_import( $job, $opt, @_ );
 
 }
+#  if ( $format eq 'simple' ) { #should be a callback or opt in FS::cdr::simple
+#    @columns = map { s/^ +//; $_; } @columns;
+#  }
 
 =back