a tollfree regex that captures 88x and works with +1
[freeside.git] / FS / FS / cdr.pm
1 package FS::cdr;
2
3 use strict;
4 use vars qw( @ISA @EXPORT_OK $DEBUG );
5 use Exporter;
6 use Tie::IxHash;
7 use Date::Parse;
8 use Date::Format;
9 use Time::Local;
10 use FS::UID qw( dbh );
11 use FS::Conf;
12 use FS::Record qw( qsearch qsearchs );
13 use FS::cdr_type;
14 use FS::cdr_calltype;
15 use FS::cdr_carrier;
16 use FS::cdr_upstream_rate;
17
18 @ISA = qw(FS::Record);
19 @EXPORT_OK = qw( _cdr_date_parser_maker _cdr_min_parser_maker );
20
21 $DEBUG = 0;
22
23 =head1 NAME
24
25 FS::cdr - Object methods for cdr records
26
27 =head1 SYNOPSIS
28
29   use FS::cdr;
30
31   $record = new FS::cdr \%hash;
32   $record = new FS::cdr { 'column' => 'value' };
33
34   $error = $record->insert;
35
36   $error = $new_record->replace($old_record);
37
38   $error = $record->delete;
39
40   $error = $record->check;
41
42 =head1 DESCRIPTION
43
44 An FS::cdr object represents an Call Data Record, typically from a telephony
45 system or provider of some sort.  FS::cdr inherits from FS::Record.  The
46 following fields are currently supported:
47
48 =over 4
49
50 =item acctid - primary key
51
52 =item calldate - Call timestamp (SQL timestamp)
53
54 =item clid - Caller*ID with text
55
56 =item src - Caller*ID number / Source number
57
58 =item dst - Destination extension
59
60 =item dcontext - Destination context
61
62 =item channel - Channel used
63
64 =item dstchannel - Destination channel if appropriate
65
66 =item lastapp - Last application if appropriate
67
68 =item lastdata - Last application data
69
70 =item startdate - Start of call (UNIX-style integer timestamp)
71
72 =item answerdate - Answer time of call (UNIX-style integer timestamp)
73
74 =item enddate - End time of call (UNIX-style integer timestamp)
75
76 =item duration - Total time in system, in seconds
77
78 =item billsec - Total time call is up, in seconds
79
80 =item disposition - What happened to the call: ANSWERED, NO ANSWER, BUSY 
81
82 =item amaflags - What flags to use: BILL, IGNORE etc, specified on a per channel basis like accountcode. 
83
84 =cut
85
86   #ignore the "omit" and "documentation" AMAs??
87   #AMA = Automated Message Accounting. 
88   #default: Sets the system default. 
89   #omit: Do not record calls. 
90   #billing: Mark the entry for billing 
91   #documentation: Mark the entry for documentation.
92
93 =item accountcode - CDR account number to use: account
94
95 =item uniqueid - Unique channel identifier (Unitel/RSLCOM Event ID)
96
97 =item userfield - CDR user-defined field
98
99 =item cdr_type - CDR type - see L<FS::cdr_type> (Usage = 1, S&E = 7, OC&C = 8)
100
101 =item charged_party - Service number to be billed
102
103 =item upstream_currency - Wholesale currency from upstream
104
105 =item upstream_price - Wholesale price from upstream
106
107 =item upstream_rateplanid - Upstream rate plan ID
108
109 =item rated_price - Rated (or re-rated) price
110
111 =item distance - km (need units field?)
112
113 =item islocal - Local - 1, Non Local = 0
114
115 =item calltypenum - Type of call - see L<FS::cdr_calltype>
116
117 =item description - Description (cdr_type 7&8 only) (used for cust_bill_pkg.itemdesc)
118
119 =item quantity - Number of items (cdr_type 7&8 only)
120
121 =item carrierid - Upstream Carrier ID (see L<FS::cdr_carrier>) 
122
123 =cut
124
125 #Telstra =1, Optus = 2, RSL COM = 3
126
127 =item upstream_rateid - Upstream Rate ID
128
129 =item svcnum - Link to customer service (see L<FS::cust_svc>)
130
131 =item freesidestatus - NULL, done (or something)
132
133 =item freesiderewritestatus - NULL, done (or something)
134
135 =item cdrbatch
136
137 =back
138
139 =head1 METHODS
140
141 =over 4
142
143 =item new HASHREF
144
145 Creates a new CDR.  To add the CDR to the database, see L<"insert">.
146
147 Note that this stores the hash reference, not a distinct copy of the hash it
148 points to.  You can ask the object for a copy with the I<hash> method.
149
150 =cut
151
152 # the new method can be inherited from FS::Record, if a table method is defined
153
154 sub table { 'cdr'; }
155
156 =item insert
157
158 Adds this record to the database.  If there is an error, returns the error,
159 otherwise returns false.
160
161 =cut
162
163 # the insert method can be inherited from FS::Record
164
165 =item delete
166
167 Delete this record from the database.
168
169 =cut
170
171 # the delete method can be inherited from FS::Record
172
173 =item replace OLD_RECORD
174
175 Replaces the OLD_RECORD with this one in the database.  If there is an error,
176 returns the error, otherwise returns false.
177
178 =cut
179
180 # the replace method can be inherited from FS::Record
181
182 =item check
183
184 Checks all fields to make sure this is a valid CDR.  If there is
185 an error, returns the error, otherwise returns false.  Called by the insert
186 and replace methods.
187
188 Note: Unlike most types of records, we don't want to "reject" a CDR and we want
189 to process them as quickly as possible, so we allow the database to check most
190 of the data.
191
192 =cut
193
194 sub check {
195   my $self = shift;
196
197 # we don't want to "reject" a CDR like other sorts of input...
198 #  my $error = 
199 #    $self->ut_numbern('acctid')
200 ##    || $self->ut_('calldate')
201 #    || $self->ut_text('clid')
202 #    || $self->ut_text('src')
203 #    || $self->ut_text('dst')
204 #    || $self->ut_text('dcontext')
205 #    || $self->ut_text('channel')
206 #    || $self->ut_text('dstchannel')
207 #    || $self->ut_text('lastapp')
208 #    || $self->ut_text('lastdata')
209 #    || $self->ut_numbern('startdate')
210 #    || $self->ut_numbern('answerdate')
211 #    || $self->ut_numbern('enddate')
212 #    || $self->ut_number('duration')
213 #    || $self->ut_number('billsec')
214 #    || $self->ut_text('disposition')
215 #    || $self->ut_number('amaflags')
216 #    || $self->ut_text('accountcode')
217 #    || $self->ut_text('uniqueid')
218 #    || $self->ut_text('userfield')
219 #    || $self->ut_numbern('cdrtypenum')
220 #    || $self->ut_textn('charged_party')
221 ##    || $self->ut_n('upstream_currency')
222 ##    || $self->ut_n('upstream_price')
223 #    || $self->ut_numbern('upstream_rateplanid')
224 ##    || $self->ut_n('distance')
225 #    || $self->ut_numbern('islocal')
226 #    || $self->ut_numbern('calltypenum')
227 #    || $self->ut_textn('description')
228 #    || $self->ut_numbern('quantity')
229 #    || $self->ut_numbern('carrierid')
230 #    || $self->ut_numbern('upstream_rateid')
231 #    || $self->ut_numbern('svcnum')
232 #    || $self->ut_textn('freesidestatus')
233 #    || $self->ut_textn('freesiderewritestatus')
234 #  ;
235 #  return $error if $error;
236
237   $self->calldate( $self->startdate_sql )
238     if !$self->calldate && $self->startdate;
239
240   #was just for $format eq 'taqua' but can't see the harm... add something to
241   #disable if it becomes a problem
242   if ( $self->duration eq '' && $self->enddate && $self->startdate ) {
243     $self->duration( $self->enddate - $self->startdate  );
244   }
245   if ( $self->billsec eq '' && $self->enddate && $self->answerdate ) {
246     $self->billsec(  $self->enddate - $self->answerdate );
247   } 
248
249   $self->set_charged_party;
250
251   #check the foreign keys even?
252   #do we want to outright *reject* the CDR?
253   my $error =
254        $self->ut_numbern('acctid')
255
256   #add a config option to turn these back on if someone needs 'em
257   #
258   #  #Usage = 1, S&E = 7, OC&C = 8
259   #  || $self->ut_foreign_keyn('cdrtypenum',  'cdr_type',     'cdrtypenum' )
260   #
261   #  #the big list in appendix 2
262   #  || $self->ut_foreign_keyn('calltypenum', 'cdr_calltype', 'calltypenum' )
263   #
264   #  # Telstra =1, Optus = 2, RSL COM = 3
265   #  || $self->ut_foreign_keyn('carrierid', 'cdr_carrier', 'carrierid' )
266   ;
267   return $error if $error;
268
269   $self->SUPER::check;
270 }
271
272 =item is_tollfree
273
274   Returns true when the cdr represents a toll free number and false otherwise.
275
276 =cut
277
278 sub is_tollfree {
279   my $self = shift;
280   ( $self->dst =~ /^(\+?1)?8(8|([02-7])\3)/ ) ? 1 : 0;
281 }
282
283 =item set_charged_party
284
285 If the charged_party field is already set, does nothing.  Otherwise:
286
287 If the cdr-charged_party-accountcode config option is enabled, sets the
288 charged_party to the accountcode.
289
290 Otherwise sets the charged_party normally: to the src field in most cases,
291 or to the dst field if it is a toll free number.
292
293 =cut
294
295 sub set_charged_party {
296   my $self = shift;
297
298   unless ( $self->charged_party ) {
299
300     my $conf = new FS::Conf;
301
302     if ( $conf->exists('cdr-charged_party-accountcode') && $self->accountcode ){
303
304       $self->charged_party( $self->accountcode );
305
306     } else {
307
308       if ( $self->is_tollfree ) {
309         $self->charged_party($self->dst);
310       } else {
311         $self->charged_party($self->src);
312       }
313
314     }
315
316   }
317
318 }
319
320 =item set_status_and_rated_price STATUS [ RATED_PRICE ]
321
322 Sets the status to the provided string.  If there is an error, returns the
323 error, otherwise returns false.
324
325 =cut
326
327 sub set_status_and_rated_price {
328   my($self, $status, $rated_price) = @_;
329   $self->freesidestatus($status);
330   $self->rated_price($rated_price);
331   $self->replace();
332 }
333
334 =item calldate_unix 
335
336 Parses the calldate in SQL string format and returns a UNIX timestamp.
337
338 =cut
339
340 sub calldate_unix {
341   str2time(shift->calldate);
342 }
343
344 =item startdate_sql
345
346 Parses the startdate in UNIX timestamp format and returns a string in SQL
347 format.
348
349 =cut
350
351 sub startdate_sql {
352   my($sec,$min,$hour,$mday,$mon,$year) = localtime(shift->startdate);
353   $mon++;
354   $year += 1900;
355   "$year-$mon-$mday $hour:$min:$sec";
356 }
357
358 =item cdr_carrier
359
360 Returns the FS::cdr_carrier object associated with this CDR, or false if no
361 carrierid is defined.
362
363 =cut
364
365 my %carrier_cache = ();
366
367 sub cdr_carrier {
368   my $self = shift;
369   return '' unless $self->carrierid;
370   $carrier_cache{$self->carrierid} ||=
371     qsearchs('cdr_carrier', { 'carrierid' => $self->carrierid } );
372 }
373
374 =item carriername 
375
376 Returns the carrier name (see L<FS::cdr_carrier>), or the empty string if
377 no FS::cdr_carrier object is assocated with this CDR.
378
379 =cut
380
381 sub carriername {
382   my $self = shift;
383   my $cdr_carrier = $self->cdr_carrier;
384   $cdr_carrier ? $cdr_carrier->carriername : '';
385 }
386
387 =item cdr_calltype
388
389 Returns the FS::cdr_calltype object associated with this CDR, or false if no
390 calltypenum is defined.
391
392 =cut
393
394 my %calltype_cache = ();
395
396 sub cdr_calltype {
397   my $self = shift;
398   return '' unless $self->calltypenum;
399   $calltype_cache{$self->calltypenum} ||=
400     qsearchs('cdr_calltype', { 'calltypenum' => $self->calltypenum } );
401 }
402
403 =item calltypename 
404
405 Returns the call type name (see L<FS::cdr_calltype>), or the empty string if
406 no FS::cdr_calltype object is assocated with this CDR.
407
408 =cut
409
410 sub calltypename {
411   my $self = shift;
412   my $cdr_calltype = $self->cdr_calltype;
413   $cdr_calltype ? $cdr_calltype->calltypename : '';
414 }
415
416 =item cdr_upstream_rate
417
418 Returns the upstream rate mapping (see L<FS::cdr_upstream_rate>), or the empty
419 string if no FS::cdr_upstream_rate object is associated with this CDR.
420
421 =cut
422
423 sub cdr_upstream_rate {
424   my $self = shift;
425   return '' unless $self->upstream_rateid;
426   qsearchs('cdr_upstream_rate', { 'upstream_rateid' => $self->upstream_rateid })
427     or '';
428 }
429
430 =item _convergent_format COLUMN [ COUNTRYCODE ]
431
432 Returns the number in COLUMN formatted as follows:
433
434 If the country code does not match COUNTRYCODE (default "61"), it is returned
435 unchanged.
436
437 If the country code does match COUNTRYCODE (default "61"), it is removed.  In
438 addiiton, "0" is prepended unless the number starts with 13, 18 or 19. (???)
439
440 =cut
441
442 sub _convergent_format {
443   my( $self, $field ) = ( shift, shift );
444   my $countrycode = scalar(@_) ? shift : '61'; #+61 = australia
445   #my $number = $self->$field();
446   my $number = $self->get($field);
447   #if ( $number =~ s/^(\+|011)$countrycode// ) {
448   if ( $number =~ s/^\+$countrycode// ) {
449     $number = "0$number"
450       unless $number =~ /^1[389]/; #???
451   }
452   $number;
453 }
454
455 =item downstream_csv [ OPTION => VALUE, ... ]
456
457 =cut
458
459 my %export_names = (
460   'convergent'      => {},
461   'simple'  => {
462     'name'           => 'Simple',
463     'invoice_header' => "Date,Time,Name,Destination,Duration,Price",
464   },
465   'simple2' => {
466     'name'           => 'Simple with source',
467     'invoice_header' => "Date,Time,Called From,Destination,Duration,Price",
468                        #"Date,Time,Name,Called From,Destination,Duration,Price",
469   },
470   'default' => {
471     'name'           => 'Default',
472     'invoice_header' => 'Date,Time,Number,Destination,Duration,Price',
473   },
474   'source_default' => {
475     'name'           => 'Default with source',
476     'invoice_header' => 'Caller,Date,Time,Number,Destination,Duration,Price',
477   },
478   'accountcode_default' => {
479     'name'           => 'Default plus accountcode',
480     'invoice_header' => 'Date,Time,Account,Number,Destination,Duration,Price',
481   },
482 );
483
484 my $duration_sub = sub {
485   my($cdr, %opt) = @_;
486   if ( $opt{minutes} ) {
487     $opt{minutes}. ( $opt{granularity} ? 'm' : ' call' );
488   } else {
489     sprintf('%.2fm', $cdr->billsec / 60 );
490   }
491 };
492
493 my %export_formats = (
494   'convergent' => [
495     'carriername', #CARRIER
496     sub { shift->_convergent_format('src') }, #SERVICE_NUMBER
497     sub { shift->_convergent_format('charged_party') }, #CHARGED_NUMBER
498     sub { time2str('%Y-%m-%d', shift->calldate_unix ) }, #DATE
499     sub { time2str('%T',       shift->calldate_unix ) }, #TIME
500     'billsec', #'duration', #DURATION
501     sub { shift->_convergent_format('dst') }, #NUMBER_DIALED
502     '', #XXX add (from prefixes in most recent email) #FROM_DESC
503     '', #XXX add (from prefixes in most recent email) #TO_DESC
504     'calltypename', #CLASS_CODE
505     'rated_price', #PRICE
506     sub { shift->rated_price ? 'Y' : 'N' }, #RATED
507     '', #OTHER_INFO
508   ],
509   'simple' => [
510     sub { time2str('%D', shift->calldate_unix ) },   #DATE
511     sub { time2str('%r', shift->calldate_unix ) },   #TIME
512     'userfield',                                     #USER
513     'dst',                                           #NUMBER_DIALED
514     $duration_sub,                                   #DURATION
515     #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
516     sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
517   ],
518   'simple2' => [
519     sub { time2str('%D', shift->calldate_unix ) },   #DATE
520     sub { time2str('%r', shift->calldate_unix ) },   #TIME
521     #'userfield',                                     #USER
522     'dst',                                           #NUMBER_DIALED
523     'src',                                           #called from
524     $duration_sub,                                   #DURATION
525     #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
526     sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
527   ],
528   'default' => [
529
530     #DATE
531     sub { time2str('%D', shift->calldate_unix ) },
532           # #time2str("%Y %b %d - %r", $cdr->calldate_unix ),
533
534     #TIME
535     sub { time2str('%r', shift->calldate_unix ) },
536           # 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
537
538     #DEST ("Number")
539     sub { my($cdr, %opt) = @_; $opt{pretty_dst} || $cdr->dst; },
540
541     #REGIONNAME ("Destination")
542     sub { my($cdr, %opt) = @_; $opt{dst_regionname}; },
543
544     #DURATION
545     $duration_sub,
546
547     #PRICE
548     sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; },
549
550   ],
551 );
552 $export_formats{'source_default'} = [ 'src', @{ $export_formats{'default'} }, ];
553 $export_formats{'accountcode_default'} =
554   [ @{ $export_formats{'default'} }[0,1],
555     'accountcode',
556     @{ $export_formats{'default'} }[2..5],
557   ];
558
559 sub downstream_csv {
560   my( $self, %opt ) = @_;
561
562   my $format = $opt{'format'}; # 'convergent';
563   return "Unknown format $format" unless exists $export_formats{$format};
564
565   #my $conf = new FS::Conf;
566   #$opt{'money_char'} ||= $conf->config('money_char') || '$';
567   $opt{'money_char'} ||= FS::Conf->new->config('money_char') || '$';
568
569   eval "use Text::CSV_XS;";
570   die $@ if $@;
571   my $csv = new Text::CSV_XS;
572
573   my @columns =
574     map {
575           ref($_) ? &{$_}($self, %opt) : $self->$_();
576         }
577     @{ $export_formats{$format} };
578
579   my $status = $csv->combine(@columns);
580   die "FS::CDR: error combining ". $csv->error_input(). "into downstream CSV"
581     unless $status;
582
583   $csv->string;
584
585 }
586
587 =back
588
589 =head1 CLASS METHODS
590
591 =over 4
592
593 =item invoice_formats
594
595 Returns an ordered list of key value pairs containing invoice format names
596 as keys (for use with part_pkg::voip_cdr) and "pretty" format names as values.
597
598 =cut
599
600 sub invoice_formats {
601   map { ($_ => $export_names{$_}->{'name'}) }
602     grep { $export_names{$_}->{'invoice_header'} }
603     keys %export_names;
604 }
605
606 =item invoice_header FORMAT
607
608 Returns a scalar containing the CSV column header for invoice format FORMAT.
609
610 =cut
611
612 sub invoice_header {
613   my $format = shift;
614   $export_names{$format}->{'invoice_header'};
615 }
616
617 =item import_formats
618
619 Returns an ordered list of key value pairs containing import format names
620 as keys (for use with batch_import) and "pretty" format names as values.
621
622 =cut
623
624 #false laziness w/part_pkg & part_export
625
626 my %cdr_info;
627 foreach my $INC ( @INC ) {
628   warn "globbing $INC/FS/cdr/*.pm\n" if $DEBUG;
629   foreach my $file ( glob("$INC/FS/cdr/*.pm") ) {
630     warn "attempting to load CDR format info from $file\n" if $DEBUG;
631     $file =~ /\/(\w+)\.pm$/ or do {
632       warn "unrecognized file in $INC/FS/cdr/: $file\n";
633       next;
634     };
635     my $mod = $1;
636     my $info = eval "use FS::cdr::$mod; ".
637                     "\\%FS::cdr::$mod\::info;";
638     if ( $@ ) {
639       die "error using FS::cdr::$mod (skipping): $@\n" if $@;
640       next;
641     }
642     unless ( keys %$info ) {
643       warn "no %info hash found in FS::cdr::$mod, skipping\n";
644       next;
645     }
646     warn "got CDR format info from FS::cdr::$mod: $info\n" if $DEBUG;
647     if ( exists($info->{'disabled'}) && $info->{'disabled'} ) {
648       warn "skipping disabled CDR format FS::cdr::$mod" if $DEBUG;
649       next;
650     }
651     $cdr_info{$mod} = $info;
652   }
653 }
654
655 tie my %import_formats, 'Tie::IxHash',
656   map  { $_ => $cdr_info{$_}->{'name'} }
657   sort { $cdr_info{$a}->{'weight'} <=> $cdr_info{$b}->{'weight'} }
658   grep { exists($cdr_info{$_}->{'import_fields'}) }
659   keys %cdr_info;
660
661 sub import_formats {
662   %import_formats;
663 }
664
665 sub _cdr_min_parser_maker {
666   my $field = shift;
667   my @fields = ref($field) ? @$field : ($field);
668   @fields = qw( billsec duration ) unless scalar(@fields) && $fields[0];
669   return sub {
670     my( $cdr, $min ) = @_;
671     my $sec = eval { _cdr_min_parse($min) };
672     die "error parsing seconds for @fields from $min minutes: $@\n" if $@;
673     $cdr->$_($sec) foreach @fields;
674   };
675 }
676
677 sub _cdr_min_parse {
678   my $min = shift;
679   sprintf('%.0f', $min * 60 );
680 }
681
682 sub _cdr_date_parser_maker {
683   my $field = shift;
684   my @fields = ref($field) ? @$field : ($field);
685   return sub {
686     my( $cdr, $datestring ) = @_;
687     my $unixdate = eval { _cdr_date_parse($datestring) };
688     die "error parsing date for @fields from $datestring: $@\n" if $@;
689     $cdr->$_($unixdate) foreach @fields;
690   };
691 }
692
693 sub _cdr_date_parse {
694   my $date = shift;
695
696   return '' unless length($date); #that's okay, it becomes NULL
697
698   my($year, $mon, $day, $hour, $min, $sec);
699
700   #$date =~ /^\s*(\d{4})[\-\/]\(\d{1,2})[\-\/](\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s*$/
701   #taqua  #2007-10-31 08:57:24.113000000
702
703   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|$)/ ) {
704     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
705   } 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|$)/ ) {
706     ($mon, $day, $year, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
707   } else {
708      die "unparsable date: $date"; #maybe we shouldn't die...
709   }
710
711   return '' if $year == 1900 && $mon == 1 && $day == 1
712             && $hour == 0    && $min == 0 && $sec == 0;
713
714   timelocal($sec, $min, $hour, $day, $mon-1, $year);
715 }
716
717 =item batch_import HASHREF
718
719 Imports CDR records.  Available options are:
720
721 =over 4
722
723 =item file
724
725 Filename
726
727 =item format
728
729 =item params
730
731 Hash reference of preset fields, typically cdrbatch
732
733 =item empty_ok
734
735 Set true to prevent throwing an error on empty imports
736
737 =back
738
739 =cut
740
741 my %import_options = (
742   'table'   => 'cdr',
743
744   'formats' => { map { $_ => $cdr_info{$_}->{'import_fields'}; }
745                      keys %cdr_info
746                },
747
748                           #drop the || 'csv' to allow auto xls for csv types?
749   'format_types' => { map { $_ => ( lc($cdr_info{$_}->{'type'}) || 'csv' ); }
750                           keys %cdr_info
751                     },
752
753   'format_headers' => { map { $_ => ( $cdr_info{$_}->{'header'} || 0 ); }
754                             keys %cdr_info
755                       },
756
757   'format_sep_chars' => { map { $_ => $cdr_info{$_}->{'sep_char'}; }
758                               keys %cdr_info
759                         },
760
761   'format_fixedlength_formats' =>
762     { map { $_ => $cdr_info{$_}->{'fixedlength_format'}; }
763           keys %cdr_info
764     },
765 );
766
767 sub _import_options {
768   \%import_options;
769 }
770
771 sub batch_import {
772   my $opt = shift;
773
774   my $iopt = _import_options;
775   $opt->{$_} = $iopt->{$_} foreach keys %$iopt;
776
777   FS::Record::batch_import( $opt );
778
779 }
780
781 =item process_batch_import
782
783 =cut
784
785 sub process_batch_import {
786   my $job = shift;
787
788   my $opt = _import_options;
789   $opt->{'params'} = [ 'format', 'cdrbatch' ];
790
791   FS::Record::process_batch_import( $job, $opt, @_ );
792
793 }
794 #  if ( $format eq 'simple' ) { #should be a callback or opt in FS::cdr::simple
795 #    @columns = map { s/^ +//; $_; } @columns;
796 #  }
797
798 =back
799
800 =head1 BUGS
801
802 =head1 SEE ALSO
803
804 L<FS::Record>, schema.html from the base documentation.
805
806 =cut
807
808 1;
809