option to do charged_party rewriting in the cdrrewrited daemon, RT#4342
[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 );
468
469 my %export_formats = (
470   'convergent' => [
471     'carriername', #CARRIER
472     sub { shift->_convergent_format('src') }, #SERVICE_NUMBER
473     sub { shift->_convergent_format('charged_party') }, #CHARGED_NUMBER
474     sub { time2str('%Y-%m-%d', shift->calldate_unix ) }, #DATE
475     sub { time2str('%T',       shift->calldate_unix ) }, #TIME
476     'billsec', #'duration', #DURATION
477     sub { shift->_convergent_format('dst') }, #NUMBER_DIALED
478     '', #XXX add (from prefixes in most recent email) #FROM_DESC
479     '', #XXX add (from prefixes in most recent email) #TO_DESC
480     'calltypename', #CLASS_CODE
481     'rated_price', #PRICE
482     sub { shift->rated_price ? 'Y' : 'N' }, #RATED
483     '', #OTHER_INFO
484   ],
485   'simple' => [
486     sub { time2str('%D', shift->calldate_unix ) },   #DATE
487     sub { time2str('%r', shift->calldate_unix ) },   #TIME
488     'userfield',                                     #USER
489     'dst',                                           #NUMBER_DIALED
490     sub { sprintf('%.2fm', shift->billsec / 60 ) },  #DURATION
491     #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
492     sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
493   ],
494   'simple2' => [
495     sub { time2str('%D', shift->calldate_unix ) },   #DATE
496     sub { time2str('%r', shift->calldate_unix ) },   #TIME
497     #'userfield',                                     #USER
498     'dst',                                           #NUMBER_DIALED
499     'src',                                           #called from
500     sub { sprintf('%.2fm', shift->billsec / 60 ) },  #DURATION
501     #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
502     sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
503   ],
504   'default' => [
505
506     #DATE
507     sub { time2str('%D', shift->calldate_unix ) },
508           # #time2str("%Y %b %d - %r", $cdr->calldate_unix ),
509
510     #TIME
511     sub { time2str('%r', shift->calldate_unix ) },
512           # 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
513
514     #DEST ("Number")
515     sub { my($cdr, %opt) = @_; $opt{pretty_dst} || $cdr->dst; },
516
517     #REGIONNAME ("Destination")
518     sub { my($cdr, %opt) = @_; $opt{dst_regionname}; },
519
520     #DURATION
521     sub { my($cdr, %opt) = @_;
522           $opt{minutes}. ( $opt{granularity} ? 'm' : ' call' );
523         },
524
525     #PRICE
526     sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; },
527
528   ],
529 );
530 $export_formats{'source_default'} = [ 'src', @{ $export_formats{'default'} }, ];
531
532 sub downstream_csv {
533   my( $self, %opt ) = @_;
534
535   my $format = $opt{'format'}; # 'convergent';
536   return "Unknown format $format" unless exists $export_formats{$format};
537
538   #my $conf = new FS::Conf;
539   #$opt{'money_char'} ||= $conf->config('money_char') || '$';
540   $opt{'money_char'} ||= FS::Conf->new->config('money_char') || '$';
541
542   eval "use Text::CSV_XS;";
543   die $@ if $@;
544   my $csv = new Text::CSV_XS;
545
546   my @columns =
547     map {
548           ref($_) ? &{$_}($self, %opt) : $self->$_();
549         }
550     @{ $export_formats{$format} };
551
552   my $status = $csv->combine(@columns);
553   die "FS::CDR: error combining ". $csv->error_input(). "into downstream CSV"
554     unless $status;
555
556   $csv->string;
557
558 }
559
560 =back
561
562 =head1 CLASS METHODS
563
564 =over 4
565
566 =item invoice_formats
567
568 Returns an ordered list of key value pairs containing invoice format names
569 as keys (for use with part_pkg::voip_cdr) and "pretty" format names as values.
570
571 =cut
572
573 sub invoice_formats {
574   map { ($_ => $export_names{$_}->{'name'}) }
575     grep { $export_names{$_}->{'invoice_header'} }
576     keys %export_names;
577 }
578
579 =item invoice_header FORMAT
580
581 Returns a scalar containing the CSV column header for invoice format FORMAT.
582
583 =cut
584
585 sub invoice_header {
586   my $format = shift;
587   $export_names{$format}->{'invoice_header'};
588 }
589
590 =item import_formats
591
592 Returns an ordered list of key value pairs containing import format names
593 as keys (for use with batch_import) and "pretty" format names as values.
594
595 =cut
596
597 #false laziness w/part_pkg & part_export
598
599 my %cdr_info;
600 foreach my $INC ( @INC ) {
601   warn "globbing $INC/FS/cdr/*.pm\n" if $DEBUG;
602   foreach my $file ( glob("$INC/FS/cdr/*.pm") ) {
603     warn "attempting to load CDR format info from $file\n" if $DEBUG;
604     $file =~ /\/(\w+)\.pm$/ or do {
605       warn "unrecognized file in $INC/FS/cdr/: $file\n";
606       next;
607     };
608     my $mod = $1;
609     my $info = eval "use FS::cdr::$mod; ".
610                     "\\%FS::cdr::$mod\::info;";
611     if ( $@ ) {
612       die "error using FS::cdr::$mod (skipping): $@\n" if $@;
613       next;
614     }
615     unless ( keys %$info ) {
616       warn "no %info hash found in FS::cdr::$mod, skipping\n";
617       next;
618     }
619     warn "got CDR format info from FS::cdr::$mod: $info\n" if $DEBUG;
620     if ( exists($info->{'disabled'}) && $info->{'disabled'} ) {
621       warn "skipping disabled CDR format FS::cdr::$mod" if $DEBUG;
622       next;
623     }
624     $cdr_info{$mod} = $info;
625   }
626 }
627
628 tie my %import_formats, 'Tie::IxHash',
629   map  { $_ => $cdr_info{$_}->{'name'} }
630   sort { $cdr_info{$a}->{'weight'} <=> $cdr_info{$b}->{'weight'} }
631   grep { exists($cdr_info{$_}->{'import_fields'}) }
632   keys %cdr_info;
633
634 sub import_formats {
635   %import_formats;
636 }
637
638 sub _cdr_min_parser_maker {
639   my $field = shift;
640   my @fields = ref($field) ? @$field : ($field);
641   @fields = qw( billsec duration ) unless scalar(@fields) && $fields[0];
642   return sub {
643     my( $cdr, $min ) = @_;
644     my $sec = eval { _cdr_min_parse($min) };
645     die "error parsing seconds for @fields from $min minutes: $@\n" if $@;
646     $cdr->$_($sec) foreach @fields;
647   };
648 }
649
650 sub _cdr_min_parse {
651   my $min = shift;
652   sprintf('%.0f', $min * 60 );
653 }
654
655 sub _cdr_date_parser_maker {
656   my $field = shift;
657   my @fields = ref($field) ? @$field : ($field);
658   return sub {
659     my( $cdr, $datestring ) = @_;
660     my $unixdate = eval { _cdr_date_parse($datestring) };
661     die "error parsing date for @fields from $datestring: $@\n" if $@;
662     $cdr->$_($unixdate) foreach @fields;
663   };
664 }
665
666 sub _cdr_date_parse {
667   my $date = shift;
668
669   return '' unless length($date); #that's okay, it becomes NULL
670
671   my($year, $mon, $day, $hour, $min, $sec);
672
673   #$date =~ /^\s*(\d{4})[\-\/]\(\d{1,2})[\-\/](\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s*$/
674   #taqua  #2007-10-31 08:57:24.113000000
675
676   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|$)/ ) {
677     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
678   } 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|$)/ ) {
679     ($mon, $day, $year, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
680   } else {
681      die "unparsable date: $date"; #maybe we shouldn't die...
682   }
683
684   return '' if $year == 1900 && $mon == 1 && $day == 1
685             && $hour == 0    && $min == 0 && $sec == 0;
686
687   timelocal($sec, $min, $hour, $day, $mon-1, $year);
688 }
689
690 =item batch_import HASHREF
691
692 Imports CDR records.  Available options are:
693
694 =over 4
695
696 =item file
697
698 Filename
699
700 =item format
701
702 =item params
703
704 Hash reference of preset fields, typically cdrbatch
705
706 =item empty_ok
707
708 Set true to prevent throwing an error on empty imports
709
710 =back
711
712 =cut
713
714 my %import_options = (
715   'table'   => 'cdr',
716
717   'formats' => { map { $_ => $cdr_info{$_}->{'import_fields'}; }
718                      keys %cdr_info
719                },
720
721                           #drop the || 'csv' to allow auto xls for csv types?
722   'format_types' => { map { $_ => ( lc($cdr_info{$_}->{'type'}) || 'csv' ); }
723                           keys %cdr_info
724                     },
725
726   'format_headers' => { map { $_ => ( $cdr_info{$_}->{'header'} || 0 ); }
727                             keys %cdr_info
728                       },
729
730   'format_sep_chars' => { map { $_ => $cdr_info{$_}->{'sep_char'}; }
731                               keys %cdr_info
732                         },
733
734   'format_fixedlength_formats' =>
735     { map { $_ => $cdr_info{$_}->{'fixedlength_format'}; }
736           keys %cdr_info
737     },
738 );
739
740 sub _import_options {
741   \%import_options;
742 }
743
744 sub batch_import {
745   my $opt = shift;
746
747   my $iopt = _import_options;
748   $opt->{$_} = $iopt->{$_} foreach keys %$iopt;
749
750   FS::Record::batch_import( $opt );
751
752 }
753
754 =item process_batch_import
755
756 =cut
757
758 sub process_batch_import {
759   my $job = shift;
760
761   my $opt = _import_options;
762   $opt->{'params'} = [ 'format', 'cdrbatch' ];
763
764   FS::Record::process_batch_import( $job, $opt, @_ );
765
766 }
767 #  if ( $format eq 'simple' ) { #should be a callback or opt in FS::cdr::simple
768 #    @columns = map { s/^ +//; $_; } @columns;
769 #  }
770
771 =back
772
773 =head1 BUGS
774
775 =head1 SEE ALSO
776
777 L<FS::Record>, schema.html from the base documentation.
778
779 =cut
780
781 1;
782