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