correct headers on accountcode_default CDR output, RT#5042
[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 set_charged_party
273
274 If the charged_party field is already set, does nothing.  Otherwise:
275
276 If the cdr-charged_party-accountcode config option is enabled, sets the
277 charged_party to the accountcode.
278
279 Otherwise sets the charged_party normally: to the src field in most cases,
280 or to the dst field if it is a toll free number.
281
282 =cut
283
284 sub set_charged_party {
285   my $self = shift;
286
287   unless ( $self->charged_party ) {
288
289     my $conf = new FS::Conf;
290
291     if ( $conf->exists('cdr-charged_party-accountcode') && $self->accountcode ){
292
293       $self->charged_party( $self->accountcode );
294
295     } else {
296
297       if ( $self->dst =~ /^(\+?1)?8[02-8]{2}/ ) {
298         $self->charged_party($self->dst);
299       } else {
300         $self->charged_party($self->src);
301       }
302
303     }
304
305   }
306
307 }
308
309 =item set_status_and_rated_price STATUS [ RATED_PRICE ]
310
311 Sets the status to the provided string.  If there is an error, returns the
312 error, otherwise returns false.
313
314 =cut
315
316 sub set_status_and_rated_price {
317   my($self, $status, $rated_price) = @_;
318   $self->freesidestatus($status);
319   $self->rated_price($rated_price);
320   $self->replace();
321 }
322
323 =item calldate_unix 
324
325 Parses the calldate in SQL string format and returns a UNIX timestamp.
326
327 =cut
328
329 sub calldate_unix {
330   str2time(shift->calldate);
331 }
332
333 =item startdate_sql
334
335 Parses the startdate in UNIX timestamp format and returns a string in SQL
336 format.
337
338 =cut
339
340 sub startdate_sql {
341   my($sec,$min,$hour,$mday,$mon,$year) = localtime(shift->startdate);
342   $mon++;
343   $year += 1900;
344   "$year-$mon-$mday $hour:$min:$sec";
345 }
346
347 =item cdr_carrier
348
349 Returns the FS::cdr_carrier object associated with this CDR, or false if no
350 carrierid is defined.
351
352 =cut
353
354 my %carrier_cache = ();
355
356 sub cdr_carrier {
357   my $self = shift;
358   return '' unless $self->carrierid;
359   $carrier_cache{$self->carrierid} ||=
360     qsearchs('cdr_carrier', { 'carrierid' => $self->carrierid } );
361 }
362
363 =item carriername 
364
365 Returns the carrier name (see L<FS::cdr_carrier>), or the empty string if
366 no FS::cdr_carrier object is assocated with this CDR.
367
368 =cut
369
370 sub carriername {
371   my $self = shift;
372   my $cdr_carrier = $self->cdr_carrier;
373   $cdr_carrier ? $cdr_carrier->carriername : '';
374 }
375
376 =item cdr_calltype
377
378 Returns the FS::cdr_calltype object associated with this CDR, or false if no
379 calltypenum is defined.
380
381 =cut
382
383 my %calltype_cache = ();
384
385 sub cdr_calltype {
386   my $self = shift;
387   return '' unless $self->calltypenum;
388   $calltype_cache{$self->calltypenum} ||=
389     qsearchs('cdr_calltype', { 'calltypenum' => $self->calltypenum } );
390 }
391
392 =item calltypename 
393
394 Returns the call type name (see L<FS::cdr_calltype>), or the empty string if
395 no FS::cdr_calltype object is assocated with this CDR.
396
397 =cut
398
399 sub calltypename {
400   my $self = shift;
401   my $cdr_calltype = $self->cdr_calltype;
402   $cdr_calltype ? $cdr_calltype->calltypename : '';
403 }
404
405 =item cdr_upstream_rate
406
407 Returns the upstream rate mapping (see L<FS::cdr_upstream_rate>), or the empty
408 string if no FS::cdr_upstream_rate object is associated with this CDR.
409
410 =cut
411
412 sub cdr_upstream_rate {
413   my $self = shift;
414   return '' unless $self->upstream_rateid;
415   qsearchs('cdr_upstream_rate', { 'upstream_rateid' => $self->upstream_rateid })
416     or '';
417 }
418
419 =item _convergent_format COLUMN [ COUNTRYCODE ]
420
421 Returns the number in COLUMN formatted as follows:
422
423 If the country code does not match COUNTRYCODE (default "61"), it is returned
424 unchanged.
425
426 If the country code does match COUNTRYCODE (default "61"), it is removed.  In
427 addiiton, "0" is prepended unless the number starts with 13, 18 or 19. (???)
428
429 =cut
430
431 sub _convergent_format {
432   my( $self, $field ) = ( shift, shift );
433   my $countrycode = scalar(@_) ? shift : '61'; #+61 = australia
434   #my $number = $self->$field();
435   my $number = $self->get($field);
436   #if ( $number =~ s/^(\+|011)$countrycode// ) {
437   if ( $number =~ s/^\+$countrycode// ) {
438     $number = "0$number"
439       unless $number =~ /^1[389]/; #???
440   }
441   $number;
442 }
443
444 =item downstream_csv [ OPTION => VALUE, ... ]
445
446 =cut
447
448 my %export_names = (
449   'convergent'      => {},
450   'simple'  => {
451     'name'           => 'Simple',
452     'invoice_header' => "Date,Time,Name,Destination,Duration,Price",
453   },
454   'simple2' => {
455     'name'           => 'Simple with source',
456     'invoice_header' => "Date,Time,Called From,Destination,Duration,Price",
457                        #"Date,Time,Name,Called From,Destination,Duration,Price",
458   },
459   'default' => {
460     'name'           => 'Default',
461     'invoice_header' => 'Date,Time,Number,Destination,Duration,Price',
462   },
463   'source_default' => {
464     'name'           => 'Default with source',
465     'invoice_header' => 'Caller,Date,Time,Number,Destination,Duration,Price',
466   },
467   'accountcode_default' => {
468     'name'           => 'Default plus accountcode',
469     'invoice_header' => 'Date,Time,Account,Number,Destination,Duration,Price',
470   },
471 );
472
473 my %export_formats = (
474   'convergent' => [
475     'carriername', #CARRIER
476     sub { shift->_convergent_format('src') }, #SERVICE_NUMBER
477     sub { shift->_convergent_format('charged_party') }, #CHARGED_NUMBER
478     sub { time2str('%Y-%m-%d', shift->calldate_unix ) }, #DATE
479     sub { time2str('%T',       shift->calldate_unix ) }, #TIME
480     'billsec', #'duration', #DURATION
481     sub { shift->_convergent_format('dst') }, #NUMBER_DIALED
482     '', #XXX add (from prefixes in most recent email) #FROM_DESC
483     '', #XXX add (from prefixes in most recent email) #TO_DESC
484     'calltypename', #CLASS_CODE
485     'rated_price', #PRICE
486     sub { shift->rated_price ? 'Y' : 'N' }, #RATED
487     '', #OTHER_INFO
488   ],
489   'simple' => [
490     sub { time2str('%D', shift->calldate_unix ) },   #DATE
491     sub { time2str('%r', shift->calldate_unix ) },   #TIME
492     'userfield',                                     #USER
493     'dst',                                           #NUMBER_DIALED
494     sub { sprintf('%.2fm', shift->billsec / 60 ) },  #DURATION
495     #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
496     sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
497   ],
498   'simple2' => [
499     sub { time2str('%D', shift->calldate_unix ) },   #DATE
500     sub { time2str('%r', shift->calldate_unix ) },   #TIME
501     #'userfield',                                     #USER
502     'dst',                                           #NUMBER_DIALED
503     'src',                                           #called from
504     sub { sprintf('%.2fm', shift->billsec / 60 ) },  #DURATION
505     #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
506     sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
507   ],
508   'default' => [
509
510     #DATE
511     sub { time2str('%D', shift->calldate_unix ) },
512           # #time2str("%Y %b %d - %r", $cdr->calldate_unix ),
513
514     #TIME
515     sub { time2str('%r', shift->calldate_unix ) },
516           # 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
517
518     #DEST ("Number")
519     sub { my($cdr, %opt) = @_; $opt{pretty_dst} || $cdr->dst; },
520
521     #REGIONNAME ("Destination")
522     sub { my($cdr, %opt) = @_; $opt{dst_regionname}; },
523
524     #DURATION
525     sub { my($cdr, %opt) = @_;
526           $opt{minutes}. ( $opt{granularity} ? 'm' : ' call' );
527         },
528
529     #PRICE
530     sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; },
531
532   ],
533 );
534 $export_formats{'source_default'} = [ 'src', @{ $export_formats{'default'} }, ];
535 $export_formats{'accountcode_default'} =
536   [ @{ $export_formats{'default'} }[0,1],
537     'accountcode',
538     @{ $export_formats{'default'} }[2..5],
539   ];
540
541 sub downstream_csv {
542   my( $self, %opt ) = @_;
543
544   my $format = $opt{'format'}; # 'convergent';
545   return "Unknown format $format" unless exists $export_formats{$format};
546
547   #my $conf = new FS::Conf;
548   #$opt{'money_char'} ||= $conf->config('money_char') || '$';
549   $opt{'money_char'} ||= FS::Conf->new->config('money_char') || '$';
550
551   eval "use Text::CSV_XS;";
552   die $@ if $@;
553   my $csv = new Text::CSV_XS;
554
555   my @columns =
556     map {
557           ref($_) ? &{$_}($self, %opt) : $self->$_();
558         }
559     @{ $export_formats{$format} };
560
561   my $status = $csv->combine(@columns);
562   die "FS::CDR: error combining ". $csv->error_input(). "into downstream CSV"
563     unless $status;
564
565   $csv->string;
566
567 }
568
569 =back
570
571 =head1 CLASS METHODS
572
573 =over 4
574
575 =item invoice_formats
576
577 Returns an ordered list of key value pairs containing invoice format names
578 as keys (for use with part_pkg::voip_cdr) and "pretty" format names as values.
579
580 =cut
581
582 sub invoice_formats {
583   map { ($_ => $export_names{$_}->{'name'}) }
584     grep { $export_names{$_}->{'invoice_header'} }
585     keys %export_names;
586 }
587
588 =item invoice_header FORMAT
589
590 Returns a scalar containing the CSV column header for invoice format FORMAT.
591
592 =cut
593
594 sub invoice_header {
595   my $format = shift;
596   $export_names{$format}->{'invoice_header'};
597 }
598
599 =item import_formats
600
601 Returns an ordered list of key value pairs containing import format names
602 as keys (for use with batch_import) and "pretty" format names as values.
603
604 =cut
605
606 #false laziness w/part_pkg & part_export
607
608 my %cdr_info;
609 foreach my $INC ( @INC ) {
610   warn "globbing $INC/FS/cdr/*.pm\n" if $DEBUG;
611   foreach my $file ( glob("$INC/FS/cdr/*.pm") ) {
612     warn "attempting to load CDR format info from $file\n" if $DEBUG;
613     $file =~ /\/(\w+)\.pm$/ or do {
614       warn "unrecognized file in $INC/FS/cdr/: $file\n";
615       next;
616     };
617     my $mod = $1;
618     my $info = eval "use FS::cdr::$mod; ".
619                     "\\%FS::cdr::$mod\::info;";
620     if ( $@ ) {
621       die "error using FS::cdr::$mod (skipping): $@\n" if $@;
622       next;
623     }
624     unless ( keys %$info ) {
625       warn "no %info hash found in FS::cdr::$mod, skipping\n";
626       next;
627     }
628     warn "got CDR format info from FS::cdr::$mod: $info\n" if $DEBUG;
629     if ( exists($info->{'disabled'}) && $info->{'disabled'} ) {
630       warn "skipping disabled CDR format FS::cdr::$mod" if $DEBUG;
631       next;
632     }
633     $cdr_info{$mod} = $info;
634   }
635 }
636
637 tie my %import_formats, 'Tie::IxHash',
638   map  { $_ => $cdr_info{$_}->{'name'} }
639   sort { $cdr_info{$a}->{'weight'} <=> $cdr_info{$b}->{'weight'} }
640   grep { exists($cdr_info{$_}->{'import_fields'}) }
641   keys %cdr_info;
642
643 sub import_formats {
644   %import_formats;
645 }
646
647 sub _cdr_min_parser_maker {
648   my $field = shift;
649   my @fields = ref($field) ? @$field : ($field);
650   @fields = qw( billsec duration ) unless scalar(@fields) && $fields[0];
651   return sub {
652     my( $cdr, $min ) = @_;
653     my $sec = eval { _cdr_min_parse($min) };
654     die "error parsing seconds for @fields from $min minutes: $@\n" if $@;
655     $cdr->$_($sec) foreach @fields;
656   };
657 }
658
659 sub _cdr_min_parse {
660   my $min = shift;
661   sprintf('%.0f', $min * 60 );
662 }
663
664 sub _cdr_date_parser_maker {
665   my $field = shift;
666   my @fields = ref($field) ? @$field : ($field);
667   return sub {
668     my( $cdr, $datestring ) = @_;
669     my $unixdate = eval { _cdr_date_parse($datestring) };
670     die "error parsing date for @fields from $datestring: $@\n" if $@;
671     $cdr->$_($unixdate) foreach @fields;
672   };
673 }
674
675 sub _cdr_date_parse {
676   my $date = shift;
677
678   return '' unless length($date); #that's okay, it becomes NULL
679
680   my($year, $mon, $day, $hour, $min, $sec);
681
682   #$date =~ /^\s*(\d{4})[\-\/]\(\d{1,2})[\-\/](\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s*$/
683   #taqua  #2007-10-31 08:57:24.113000000
684
685   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|$)/ ) {
686     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
687   } 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|$)/ ) {
688     ($mon, $day, $year, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
689   } else {
690      die "unparsable date: $date"; #maybe we shouldn't die...
691   }
692
693   return '' if $year == 1900 && $mon == 1 && $day == 1
694             && $hour == 0    && $min == 0 && $sec == 0;
695
696   timelocal($sec, $min, $hour, $day, $mon-1, $year);
697 }
698
699 =item batch_import HASHREF
700
701 Imports CDR records.  Available options are:
702
703 =over 4
704
705 =item file
706
707 Filename
708
709 =item format
710
711 =item params
712
713 Hash reference of preset fields, typically cdrbatch
714
715 =item empty_ok
716
717 Set true to prevent throwing an error on empty imports
718
719 =back
720
721 =cut
722
723 my %import_options = (
724   'table'   => 'cdr',
725
726   'formats' => { map { $_ => $cdr_info{$_}->{'import_fields'}; }
727                      keys %cdr_info
728                },
729
730                           #drop the || 'csv' to allow auto xls for csv types?
731   'format_types' => { map { $_ => ( lc($cdr_info{$_}->{'type'}) || 'csv' ); }
732                           keys %cdr_info
733                     },
734
735   'format_headers' => { map { $_ => ( $cdr_info{$_}->{'header'} || 0 ); }
736                             keys %cdr_info
737                       },
738
739   'format_sep_chars' => { map { $_ => $cdr_info{$_}->{'sep_char'}; }
740                               keys %cdr_info
741                         },
742
743   'format_fixedlength_formats' =>
744     { map { $_ => $cdr_info{$_}->{'fixedlength_format'}; }
745           keys %cdr_info
746     },
747 );
748
749 sub _import_options {
750   \%import_options;
751 }
752
753 sub batch_import {
754   my $opt = shift;
755
756   my $iopt = _import_options;
757   $opt->{$_} = $iopt->{$_} foreach keys %$iopt;
758
759   FS::Record::batch_import( $opt );
760
761 }
762
763 =item process_batch_import
764
765 =cut
766
767 sub process_batch_import {
768   my $job = shift;
769
770   my $opt = _import_options;
771   $opt->{'params'} = [ 'format', 'cdrbatch' ];
772
773   FS::Record::process_batch_import( $job, $opt, @_ );
774
775 }
776 #  if ( $format eq 'simple' ) { #should be a callback or opt in FS::cdr::simple
777 #    @columns = map { s/^ +//; $_; } @columns;
778 #  }
779
780 =back
781
782 =head1 BUGS
783
784 =head1 SEE ALSO
785
786 L<FS::Record>, schema.html from the base documentation.
787
788 =cut
789
790 1;
791