voxlinesystems CDRs and quantity bs
[freeside.git] / FS / FS / cdr.pm
1 package FS::cdr;
2
3 use strict;
4 use vars qw( @ISA );
5 use Date::Parse;
6 use Date::Format;
7 use Time::Local;
8 use FS::UID qw( dbh );
9 use FS::Record qw( qsearch qsearchs );
10 use FS::cdr_type;
11 use FS::cdr_calltype;
12 use FS::cdr_carrier;
13 use FS::cdr_upstream_rate;
14
15 @ISA = qw(FS::Record);
16
17 =head1 NAME
18
19 FS::cdr - Object methods for cdr records
20
21 =head1 SYNOPSIS
22
23   use FS::cdr;
24
25   $record = new FS::cdr \%hash;
26   $record = new FS::cdr { 'column' => 'value' };
27
28   $error = $record->insert;
29
30   $error = $new_record->replace($old_record);
31
32   $error = $record->delete;
33
34   $error = $record->check;
35
36 =head1 DESCRIPTION
37
38 An FS::cdr object represents an Call Data Record, typically from a telephony
39 system or provider of some sort.  FS::cdr inherits from FS::Record.  The
40 following fields are currently supported:
41
42 =over 4
43
44 =item acctid - primary key
45
46 =item calldate - Call timestamp (SQL timestamp)
47
48 =item clid - Caller*ID with text
49
50 =item src - Caller*ID number / Source number
51
52 =item dst - Destination extension
53
54 =item dcontext - Destination context
55
56 =item channel - Channel used
57
58 =item dstchannel - Destination channel if appropriate
59
60 =item lastapp - Last application if appropriate
61
62 =item lastdata - Last application data
63
64 =item startdate - Start of call (UNIX-style integer timestamp)
65
66 =item answerdate - Answer time of call (UNIX-style integer timestamp)
67
68 =item enddate - End time of call (UNIX-style integer timestamp)
69
70 =item duration - Total time in system, in seconds
71
72 =item billsec - Total time call is up, in seconds
73
74 =item disposition - What happened to the call: ANSWERED, NO ANSWER, BUSY 
75
76 =item amaflags - What flags to use: BILL, IGNORE etc, specified on a per channel basis like accountcode. 
77
78 =cut
79
80   #ignore the "omit" and "documentation" AMAs??
81   #AMA = Automated Message Accounting. 
82   #default: Sets the system default. 
83   #omit: Do not record calls. 
84   #billing: Mark the entry for billing 
85   #documentation: Mark the entry for documentation.
86
87 =item accountcode - CDR account number to use: account
88
89 =item uniqueid - Unique channel identifier (Unitel/RSLCOM Event ID)
90
91 =item userfield - CDR user-defined field
92
93 =item cdr_type - CDR type - see L<FS::cdr_type> (Usage = 1, S&E = 7, OC&C = 8)
94
95 =item charged_party - Service number to be billed
96
97 =item upstream_currency - Wholesale currency from upstream
98
99 =item upstream_price - Wholesale price from upstream
100
101 =item upstream_rateplanid - Upstream rate plan ID
102
103 =item rated_price - Rated (or re-rated) price
104
105 =item distance - km (need units field?)
106
107 =item islocal - Local - 1, Non Local = 0
108
109 =item calltypenum - Type of call - see L<FS::cdr_calltype>
110
111 =item description - Description (cdr_type 7&8 only) (used for cust_bill_pkg.itemdesc)
112
113 =item quantity - Number of items (cdr_type 7&8 only)
114
115 =item carrierid - Upstream Carrier ID (see L<FS::cdr_carrier>) 
116
117 =cut
118
119 #Telstra =1, Optus = 2, RSL COM = 3
120
121 =item upstream_rateid - Upstream Rate ID
122
123 =item svcnum - Link to customer service (see L<FS::cust_svc>)
124
125 =item freesidestatus - NULL, done (or something)
126
127 =back
128
129 =head1 METHODS
130
131 =over 4
132
133 =item new HASHREF
134
135 Creates a new CDR.  To add the CDR to the database, see L<"insert">.
136
137 Note that this stores the hash reference, not a distinct copy of the hash it
138 points to.  You can ask the object for a copy with the I<hash> method.
139
140 =cut
141
142 # the new method can be inherited from FS::Record, if a table method is defined
143
144 sub table { 'cdr'; }
145
146 =item insert
147
148 Adds this record to the database.  If there is an error, returns the error,
149 otherwise returns false.
150
151 =cut
152
153 # the insert method can be inherited from FS::Record
154
155 =item delete
156
157 Delete this record from the database.
158
159 =cut
160
161 # the delete method can be inherited from FS::Record
162
163 =item replace OLD_RECORD
164
165 Replaces the OLD_RECORD with this one in the database.  If there is an error,
166 returns the error, otherwise returns false.
167
168 =cut
169
170 # the replace method can be inherited from FS::Record
171
172 =item check
173
174 Checks all fields to make sure this is a valid CDR.  If there is
175 an error, returns the error, otherwise returns false.  Called by the insert
176 and replace methods.
177
178 Note: Unlike most types of records, we don't want to "reject" a CDR and we want
179 to process them as quickly as possible, so we allow the database to check most
180 of the data.
181
182 =cut
183
184 sub check {
185   my $self = shift;
186
187 # we don't want to "reject" a CDR like other sorts of input...
188 #  my $error = 
189 #    $self->ut_numbern('acctid')
190 ##    || $self->ut_('calldate')
191 #    || $self->ut_text('clid')
192 #    || $self->ut_text('src')
193 #    || $self->ut_text('dst')
194 #    || $self->ut_text('dcontext')
195 #    || $self->ut_text('channel')
196 #    || $self->ut_text('dstchannel')
197 #    || $self->ut_text('lastapp')
198 #    || $self->ut_text('lastdata')
199 #    || $self->ut_numbern('startdate')
200 #    || $self->ut_numbern('answerdate')
201 #    || $self->ut_numbern('enddate')
202 #    || $self->ut_number('duration')
203 #    || $self->ut_number('billsec')
204 #    || $self->ut_text('disposition')
205 #    || $self->ut_number('amaflags')
206 #    || $self->ut_text('accountcode')
207 #    || $self->ut_text('uniqueid')
208 #    || $self->ut_text('userfield')
209 #    || $self->ut_numbern('cdrtypenum')
210 #    || $self->ut_textn('charged_party')
211 ##    || $self->ut_n('upstream_currency')
212 ##    || $self->ut_n('upstream_price')
213 #    || $self->ut_numbern('upstream_rateplanid')
214 ##    || $self->ut_n('distance')
215 #    || $self->ut_numbern('islocal')
216 #    || $self->ut_numbern('calltypenum')
217 #    || $self->ut_textn('description')
218 #    || $self->ut_numbern('quantity')
219 #    || $self->ut_numbern('carrierid')
220 #    || $self->ut_numbern('upstream_rateid')
221 #    || $self->ut_numbern('svcnum')
222 #    || $self->ut_textn('freesidestatus')
223 #  ;
224 #  return $error if $error;
225
226   $self->calldate( $self->startdate_sql )
227     if !$self->calldate && $self->startdate;
228
229   unless ( $self->charged_party ) {
230     if ( $self->dst =~ /^(\+?1)?8[02-8]{2}/ ) {
231       $self->charged_party($self->dst);
232     } else {
233       $self->charged_party($self->src);
234     }
235   }
236
237   #check the foreign keys even?
238   #do we want to outright *reject* the CDR?
239   my $error =
240        $self->ut_numbern('acctid')
241
242     #Usage = 1, S&E = 7, OC&C = 8
243     || $self->ut_foreign_keyn('cdrtypenum',  'cdr_type',     'cdrtypenum' )
244
245     #the big list in appendix 2
246     || $self->ut_foreign_keyn('calltypenum', 'cdr_calltype', 'calltypenum' )
247
248     # Telstra =1, Optus = 2, RSL COM = 3
249     || $self->ut_foreign_keyn('carrierid', 'cdr_carrier', 'carrierid' )
250   ;
251   return $error if $error;
252
253   $self->SUPER::check;
254 }
255
256 =item set_status_and_rated_price STATUS [ RATED_PRICE ]
257
258 Sets the status to the provided string.  If there is an error, returns the
259 error, otherwise returns false.
260
261 =cut
262
263 sub set_status_and_rated_price {
264   my($self, $status, $rated_price) = @_;
265   $self->freesidestatus($status);
266   $self->rated_price($rated_price);
267   $self->replace();
268 }
269
270 =item calldate_unix 
271
272 Parses the calldate in SQL string format and returns a UNIX timestamp.
273
274 =cut
275
276 sub calldate_unix {
277   str2time(shift->calldate);
278 }
279
280 =item startdate_sql
281
282 Parses the startdate in UNIX timestamp format and returns a string in SQL
283 format.
284
285 =cut
286
287 sub startdate_sql {
288   my($sec,$min,$hour,$mday,$mon,$year) = localtime(shift->startdate);
289   $mon++;
290   $year += 1900;
291   "$year-$mon-$mday $hour:$min:$sec";
292 }
293
294 =item cdr_carrier
295
296 Returns the FS::cdr_carrier object associated with this CDR, or false if no
297 carrierid is defined.
298
299 =cut
300
301 my %carrier_cache = ();
302
303 sub cdr_carrier {
304   my $self = shift;
305   return '' unless $self->carrierid;
306   $carrier_cache{$self->carrierid} ||=
307     qsearchs('cdr_carrier', { 'carrierid' => $self->carrierid } );
308 }
309
310 =item carriername 
311
312 Returns the carrier name (see L<FS::cdr_carrier>), or the empty string if
313 no FS::cdr_carrier object is assocated with this CDR.
314
315 =cut
316
317 sub carriername {
318   my $self = shift;
319   my $cdr_carrier = $self->cdr_carrier;
320   $cdr_carrier ? $cdr_carrier->carriername : '';
321 }
322
323 =item cdr_calltype
324
325 Returns the FS::cdr_calltype object associated with this CDR, or false if no
326 calltypenum is defined.
327
328 =cut
329
330 my %calltype_cache = ();
331
332 sub cdr_calltype {
333   my $self = shift;
334   return '' unless $self->calltypenum;
335   $calltype_cache{$self->calltypenum} ||=
336     qsearchs('cdr_calltype', { 'calltypenum' => $self->calltypenum } );
337 }
338
339 =item calltypename 
340
341 Returns the call type name (see L<FS::cdr_calltype>), or the empty string if
342 no FS::cdr_calltype object is assocated with this CDR.
343
344 =cut
345
346 sub calltypename {
347   my $self = shift;
348   my $cdr_calltype = $self->cdr_calltype;
349   $cdr_calltype ? $cdr_calltype->calltypename : '';
350 }
351
352 =item cdr_upstream_rate
353
354 Returns the upstream rate mapping (see L<FS::cdr_upstream_rate>), or the empty
355 string if no FS::cdr_upstream_rate object is associated with this CDR.
356
357 =cut
358
359 sub cdr_upstream_rate {
360   my $self = shift;
361   return '' unless $self->upstream_rateid;
362   qsearchs('cdr_upstream_rate', { 'upstream_rateid' => $self->upstream_rateid })
363     or '';
364 }
365
366 =item _convergent_format COLUMN [ COUNTRYCODE ]
367
368 Returns the number in COLUMN formatted as follows:
369
370 If the country code does not match COUNTRYCODE (default "61"), it is returned
371 unchanged.
372
373 If the country code does match COUNTRYCODE (default "61"), it is removed.  In
374 addiiton, "0" is prepended unless the number starts with 13, 18 or 19. (???)
375
376 =cut
377
378 sub _convergent_format {
379   my( $self, $field ) = ( shift, shift );
380   my $countrycode = scalar(@_) ? shift : '61'; #+61 = australia
381   #my $number = $self->$field();
382   my $number = $self->get($field);
383   #if ( $number =~ s/^(\+|011)$countrycode// ) {
384   if ( $number =~ s/^\+$countrycode// ) {
385     $number = "0$number"
386       unless $number =~ /^1[389]/; #???
387   }
388   $number;
389 }
390
391 =item downstream_csv [ OPTION => VALUE, ... ]
392
393 =cut
394
395 my %export_names = (
396   'convergent'      => {},
397   'voxlinesystems'  => { 'name'           => 'VoxLineSystems',
398                          'invoice_header' =>
399                            "Date,Time,Name,Destination,Duration,Price",
400                        },
401   'voxlinesystems2' => { 'name'           => 'VoxLineSystems with source',
402                          'invoice_header' =>
403                            "Date,Time,Name,Destination,Called From,Duration,Price",
404                        },
405 );
406
407 my %export_formats = (
408   'convergent' => [
409     'carriername', #CARRIER
410     sub { shift->_convergent_format('src') }, #SERVICE_NUMBER
411     sub { shift->_convergent_format('charged_party') }, #CHARGED_NUMBER
412     sub { time2str('%Y-%m-%d', shift->calldate_unix ) }, #DATE
413     sub { time2str('%T',       shift->calldate_unix ) }, #TIME
414     'billsec', #'duration', #DURATION
415     sub { shift->_convergent_format('dst') }, #NUMBER_DIALED
416     '', #XXX add (from prefixes in most recent email) #FROM_DESC
417     '', #XXX add (from prefixes in most recent email) #TO_DESC
418     'calltypename', #CLASS_CODE
419     'rated_price', #PRICE
420     sub { shift->rated_price ? 'Y' : 'N' }, #RATED
421     '', #OTHER_INFO
422   ],
423   'voxlinesystems' => [
424     sub { time2str('%D', shift->calldate_unix ) },   #DATE
425     sub { time2str('%r', shift->calldate_unix ) },   #TIME
426     'userfield',                                     #USER
427     'dst',                                           #NUMBER_DIALED
428     sub { sprintf('%.2fm', shift->billsec / 60 ) },  #DURATION
429     sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
430   ],
431   'voxlinesystems2' => [
432     sub { time2str('%D', shift->calldate_unix ) },   #DATE
433     sub { time2str('%T', shift->calldate_unix ) },   #TIME
434     'userfield',                                     #USER
435     'dst',                                           #NUMBER_DIALED
436     'src',                                           #called from
437     sub { sprintf('%.2fm', shift->billsec / 60 ) },  #DURATION
438     sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
439   ],
440 );
441
442 sub downstream_csv {
443   my( $self, %opt ) = @_;
444
445   my $format = $opt{'format'}; # 'convergent';
446   return "Unknown format $format" unless exists $export_formats{$format};
447
448   eval "use Text::CSV_XS;";
449   die $@ if $@;
450   my $csv = new Text::CSV_XS;
451
452   my @columns =
453     map {
454           ref($_) ? &{$_}($self) : $self->$_();
455         }
456     @{ $export_formats{$format} };
457
458   my $status = $csv->combine(@columns);
459   die "FS::CDR: error combining ". $csv->error_input(). "into downstream CSV"
460     unless $status;
461
462   $csv->string;
463
464 }
465
466 =back
467
468 =head1 CLASS METHODS
469
470 =over 4
471
472 =item invoice_formats
473
474 Returns an ordered list of key value pairs containing invoice format names
475 as keys (for use with part_pkg::voip_cdr) and "pretty" format names as values.
476
477 =cut
478
479 sub invoice_formats {
480   map { ($_ => $export_names{$_}->{'name'}) }
481     grep { $export_names{$_}->{'invoice_header'} }
482     keys %export_names;
483 }
484
485 =item invoice_header FORMAT
486
487 Returns a scalar containing the CSV column header for invoice format FORMAT.
488
489 =cut
490
491 sub invoice_header {
492   my $format = shift;
493   $export_names{$format}->{'invoice_header'};
494 }
495
496 =item import_formats
497
498 Returns an ordered list of key value pairs containing import format names
499 as keys (for use with batch_import) and "pretty" format names as values.
500
501 =cut
502
503 sub import_formats {
504   (
505     'asterisk'       => 'Asterisk',
506     'taqua'          => 'Taqua',
507     'unitel'         => 'Unitel/RSLCOM',
508     'voxlinesystems' => 'VoxLineSystems',  #XXX? get the actual vendor name
509     'simple'         => 'Simple',
510   );
511 }
512
513 my($tmp_mday, $tmp_mon, $tmp_year);
514
515 sub _cdr_date_parser_maker {
516   my $field = shift;
517   return sub {
518     my( $cdr, $date ) = @_;
519     #$cdr->$field( _cdr_date_parse($date) );
520     eval { $cdr->$field( _cdr_date_parse($date) ); };
521     die "error parsing date for $field from $date: $@\n" if $@;
522   };
523 }
524
525 sub _cdr_date_parse {
526   my $date = shift;
527
528   return '' unless length($date); #that's okay, it becomes NULL
529
530   #$date =~ /^\s*(\d{4})[\-\/]\(\d{1,2})[\-\/](\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s*$/
531   $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|$)/
532     or die "unparsable date: $date"; #maybe we shouldn't die...
533   my($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
534
535   return '' if $year == 1900 && $mon == 1 && $day == 1
536             && $hour == 0    && $min == 0 && $sec == 0;
537
538   timelocal($sec, $min, $hour, $day, $mon-1, $year);
539 }
540
541 #taqua  #2007-10-31 08:57:24.113000000
542
543 #http://www.the-asterisk-book.com/unstable/funktionen-cdr.html
544 my %amaflags = (
545   DEFAULT       => 0,
546   OMIT          => 1, #asterisk 1.4+
547   IGNORE        => 1, #asterisk 1.2
548   BILLING       => 2, #asterisk 1.4+
549   BILL          => 2, #asterisk 1.2
550   DOCUMENTATION => 3,
551   #? '' => 0,
552 );
553
554 my %import_formats = (
555   'asterisk' => [
556     'accountcode',
557     'src',
558     'dst',
559     'dcontext',
560     'clid',
561     'channel',
562     'dstchannel',
563     'lastapp',
564     'lastdata',
565     _cdr_date_parser_maker('startdate'),
566     _cdr_date_parser_maker('answerdate'),
567     _cdr_date_parser_maker('enddate'),
568     'duration',
569     'billsec',
570     'disposition',
571     sub { my($cdr, $amaflags) = @_; $cdr->amaflags($amaflags{$amaflags}); },
572     'uniqueid',
573     'userfield',
574   ],
575   'taqua' => [ #some of these are kind arbitrary...
576
577     sub { my($cdr, $field) = @_; },       #XXX interesting RecordType
578              # easy to fix: Can't find cdr.cdrtypenum 1 in cdr_type.cdrtypenum
579
580     sub { my($cdr, $field) = @_; },             #all10#RecordVersion
581     sub { my($cdr, $field) = @_; },       #OrigShelfNumber
582     sub { my($cdr, $field) = @_; },       #OrigCardNumber
583     sub { my($cdr, $field) = @_; },       #OrigCircuit
584     sub { my($cdr, $field) = @_; },       #OrigCircuitType
585     'uniqueid',                           #SequenceNumber
586     'accountcode',                        #SessionNumber
587     'src',                                #CallingPartyNumber
588     'dst',                                #CalledPartyNumber
589     _cdr_date_parser_maker('startdate'),  #CallArrivalTime
590     _cdr_date_parser_maker('enddate'),    #CallCompletionTime
591
592     #Disposition
593     #sub { my($cdr, $d ) = @_; $cdr->disposition( $disposition{$d}): },
594     'disposition',
595                                           #  -1 => '',
596                                           #   0 => '',
597                                           # 100 => '',
598                                           # 101 => '',
599                                           # 102 => '',
600                                           # 103 => '',
601                                           # 104 => '',
602                                           # 105 => '',
603                                           # 201 => '',
604                                           # 203 => '',
605
606     _cdr_date_parser_maker('answerdate'), #DispositionTime
607     sub { my($cdr, $field) = @_; },       #TCAP
608     sub { my($cdr, $field) = @_; },       #OutboundCarrierConnectTime
609     sub { my($cdr, $field) = @_; },       #OutboundCarrierDisconnectTime
610
611     #TermTrunkGroup
612     #it appears channels are actually part of trunk groups, but this data
613     #is interesting and we need a source and destination place to put it
614     'dstchannel',                         #TermTrunkGroup
615
616
617     sub { my($cdr, $field) = @_; },       #TermShelfNumber
618     sub { my($cdr, $field) = @_; },       #TermCardNumber
619     sub { my($cdr, $field) = @_; },       #TermCircuit
620     sub { my($cdr, $field) = @_; },       #TermCircuitType
621     sub { my($cdr, $field) = @_; },       #OutboundCarrierId
622     'charged_party',                      #BillingNumber
623     sub { my($cdr, $field) = @_; },       #SubscriberNumber
624     'lastapp',                            #ServiceName
625     sub { my($cdr, $field) = @_; },       #some weirdness #ChargeTime
626     'lastdata',                           #ServiceInformation
627     sub { my($cdr, $field) = @_; },       #FacilityInfo
628     sub { my($cdr, $field) = @_; },             #all 1900-01-01 0#CallTraceTime
629     sub { my($cdr, $field) = @_; },             #all-1#UniqueIndicator
630     sub { my($cdr, $field) = @_; },             #all-1#PresentationIndicator
631     sub { my($cdr, $field) = @_; },             #empty#Pin
632     sub { my($cdr, $field) = @_; },       #CallType
633     sub { my($cdr, $field) = @_; },           #Balt/empty #OrigRateCenter
634     sub { my($cdr, $field) = @_; },           #Balt/empty #TermRateCenter
635
636     #OrigTrunkGroup
637     #it appears channels are actually part of trunk groups, but this data
638     #is interesting and we need a source and destination place to put it
639     'channel',                            #OrigTrunkGroup
640
641     'userfield',                                #empty#UserDefined
642     sub { my($cdr, $field) = @_; },             #empty#PseudoDestinationNumber
643     sub { my($cdr, $field) = @_; },             #all-1#PseudoCarrierCode
644     sub { my($cdr, $field) = @_; },             #empty#PseudoANI
645     sub { my($cdr, $field) = @_; },             #all-1#PseudoFacilityInfo
646     sub { my($cdr, $field) = @_; },       #OrigDialedDigits
647     sub { my($cdr, $field) = @_; },             #all-1#OrigOutboundCarrier
648     sub { my($cdr, $field) = @_; },       #IncomingCarrierID
649     'dcontext',                           #JurisdictionInfo
650     sub { my($cdr, $field) = @_; },       #OrigDestDigits
651     sub { my($cdr, $field) = @_; },       #huh?#InsertTime
652     sub { my($cdr, $field) = @_; },       #key
653     sub { my($cdr, $field) = @_; },             #empty#AMALineNumber
654     sub { my($cdr, $field) = @_; },             #empty#AMAslpID
655     sub { my($cdr, $field) = @_; },             #empty#AMADigitsDialedWC
656     sub { my($cdr, $field) = @_; },       #OpxOffHook
657     sub { my($cdr, $field) = @_; },       #OpxOnHook
658
659         #acctid - primary key
660   #AUTO #calldate - Call timestamp (SQL timestamp)
661 #clid - Caller*ID with text
662         #XXX src - Caller*ID number / Source number
663         #XXX dst - Destination extension
664         #dcontext - Destination context
665         #channel - Channel used
666         #dstchannel - Destination channel if appropriate
667         #lastapp - Last application if appropriate
668         #lastdata - Last application data
669         #startdate - Start of call (UNIX-style integer timestamp)
670         #answerdate - Answer time of call (UNIX-style integer timestamp)
671         #enddate - End time of call (UNIX-style integer timestamp)
672   #HACK#duration - Total time in system, in seconds
673   #HACK#XXX billsec - Total time call is up, in seconds
674         #disposition - What happened to the call: ANSWERED, NO ANSWER, BUSY
675 #INT amaflags - What flags to use: BILL, IGNORE etc, specified on a per channel basis like accountcode.
676         #accountcode - CDR account number to use: account
677
678         #uniqueid - Unique channel identifier (Unitel/RSLCOM Event ID)
679         #userfield - CDR user-defined field
680
681         #X cdrtypenum - CDR type - see FS::cdr_type (Usage = 1, S&E = 7, OC&C = 8)
682         #XXX charged_party - Service number to be billed
683 #upstream_currency - Wholesale currency from upstream
684 #X upstream_price - Wholesale price from upstream
685 #upstream_rateplanid - Upstream rate plan ID
686 #rated_price - Rated (or re-rated) price
687 #distance - km (need units field?)
688 #islocal - Local - 1, Non Local = 0
689 #calltypenum - Type of call - see FS::cdr_calltype
690 #X description - Description (cdr_type 7&8 only) (used for cust_bill_pkg.itemdesc)
691 #quantity - Number of items (cdr_type 7&8 only)
692 #carrierid - Upstream Carrier ID (see FS::cdr_carrier)
693 #upstream_rateid - Upstream Rate ID
694
695         #svcnum - Link to customer service (see FS::cust_svc)
696         #freesidestatus - NULL, done (or something)
697
698   ],
699   'unitel' => [
700     'uniqueid',
701     #'cdr_type',
702     'cdrtypenum',
703     'calldate', # may need massaging?  huh maybe not...
704     #'billsec', #XXX duration and billsec?
705                 sub { $_[0]->billsec(  $_[1] );
706                       $_[0]->duration( $_[1] );
707                     },
708     'src',
709     'dst', # XXX needs to have "+61" prepended unless /^\+/ ???
710     'charged_party',
711     'upstream_currency',
712     'upstream_price',
713     'upstream_rateplanid',
714     'distance',
715     'islocal',
716     'calltypenum',
717     'startdate',  #XXX needs massaging
718     'enddate',    #XXX same
719     'description',
720     'quantity',
721     'carrierid',
722     'upstream_rateid',
723   ],
724   'voxlinesystems' => [ #XXX get the actual vendor name
725     'disposition',                        #Status
726     'startdate',                          #Start (what do you know, a timestamp!
727     sub { my($cdr, $field) = @_; },       #Start date
728     sub { my($cdr, $field) = @_; },       #Start time
729     'enddate',                            #End (also a timestamp!)
730     sub { my($cdr, $field) = @_; },       #End date
731     sub { my($cdr, $field) = @_; },       #End time
732     'accountcode',                        #Calling customer... map to agent_custid??
733     sub { my($cdr, $field) = @_; },       #Calling type
734     'src',
735     'userfield',                          #Calling name #?
736     sub { my($cdr, $field) = @_; },       #Called type
737     'dst',                                #Called number
738     sub { my($cdr, $field) = @_; },       #Destination customer
739     sub { my($cdr, $field) = @_; },       #Destination type
740     sub { my($cdr, $field) = @_; },       #Destination Number
741     sub { my($cdr, $field) = @_; },       #Inbound calling type
742     sub { my($cdr, $field) = @_; },       #Inbound calling number
743     sub { my($cdr, $field) = @_; },       #Inbound called type
744     sub { my($cdr, $field) = @_; },       #Inbound called number
745     sub { my($cdr, $field) = @_; },       #Inbound destination type
746     sub { my($cdr, $field) = @_; },       #Inbound destination number
747     sub { my($cdr, $field) = @_; },       #Outbound calling type
748     sub { my($cdr, $field) = @_; },       #Outbound calling number
749     sub { my($cdr, $field) = @_; },       #Outbound called type
750     sub { my($cdr, $field) = @_; },       #Outbound called number
751     sub { my($cdr, $field) = @_; },       #Outbound destination type
752     sub { my($cdr, $field) = @_; },       #Outbound destination number
753     sub { my($cdr, $field) = @_; },       #Internal calling type
754     sub { my($cdr, $field) = @_; },       #Internal calling number
755     sub { my($cdr, $field) = @_; },       #Internal called type
756     sub { my($cdr, $field) = @_; },       #Internal called number
757     sub { my($cdr, $field) = @_; },       #Internal destination type
758     sub { my($cdr, $field) = @_; },       #Internal destination number
759     'duration',                           #Total seconds
760     sub { my($cdr, $field) = @_; },       #Ring seconds
761     'billsec',                            #Billable seconds
762     'upstream_price',                     #Cost
763     sub { my($cdr, $field) = @_; },       #Billing customer
764     sub { my($cdr, $field) = @_; },       #Billing customer name
765     sub { my($cdr, $field) = @_; },       #Billing type
766     sub { my($cdr, $field) = @_; },       #Billing reference
767   ],
768   'simple' => [
769
770     # Date
771     sub { my($cdr, $date) = @_;
772           $date =~ /^(\d{1,2})\/(\d{1,2})\/(\d\d(\d\d)?)$/
773             or die "unparsable date: $date"; #maybe we shouldn't die...
774           #$cdr->startdate( timelocal(0, 0, 0 ,$2, $1-1, $3) );
775           ($tmp_mday, $tmp_mon, $tmp_year) = ( $2, $1-1, $3 );
776         },
777
778     # Time
779     sub { my($cdr, $time) = @_;
780           #my($sec, $min, $hour, $mday, $mon, $year)= localtime($cdr->startdate);
781           $time =~ /^(\d{1,2}):(\d{1,2}):(\d{1,2})$/
782             or die "unparsable time: $time"; #maybe we shouldn't die...
783           #$cdr->startdate( timelocal($3, $2, $1 ,$mday, $mon, $year) );
784           $cdr->startdate(
785             timelocal($3, $2, $1 ,$tmp_mday, $tmp_mon, $tmp_year)
786           );
787         },
788
789     # Source_Number
790     'src',
791
792     # Terminating_Number
793     'dst',
794
795     # Duration
796     sub { my($cdr, $min) = @_;
797           my $sec = sprintf('%.0f', $min * 60 );
798           $cdr->billsec(  $sec );
799           $cdr->duration( $sec );
800         },
801
802   ],
803 );
804
805 my %import_header = (
806   'simple'         => 1,
807   'taqua'          => 1,
808   'voxlinesystems' => 2, #XXX vendor name
809 );
810
811 =item batch_import HASHREF
812
813 Imports CDR records.  Available options are:
814
815 =over 4
816
817 =item filehandle
818
819 =item format
820
821 =back
822
823 =cut
824
825 sub batch_import {
826   my $param = shift;
827
828   my $fh = $param->{filehandle};
829   my $format = $param->{format};
830
831   return "Unknown format $format" unless exists $import_formats{$format};
832
833   eval "use Text::CSV_XS;";
834   die $@ if $@;
835
836   my $csv = new Text::CSV_XS;
837
838   my $imported = 0;
839   #my $columns;
840
841   local $SIG{HUP} = 'IGNORE';
842   local $SIG{INT} = 'IGNORE';
843   local $SIG{QUIT} = 'IGNORE';
844   local $SIG{TERM} = 'IGNORE';
845   local $SIG{TSTP} = 'IGNORE';
846   local $SIG{PIPE} = 'IGNORE';
847
848   my $oldAutoCommit = $FS::UID::AutoCommit;
849   local $FS::UID::AutoCommit = 0;
850   my $dbh = dbh;
851
852   my $header_lines =
853     exists($import_header{$format}) ? $import_header{$format} : 0;
854
855   my $line;
856   while ( defined($line=<$fh>) ) {
857
858     next if $header_lines-- > 0; #&& $line =~ /^[\w, "]+$/ 
859
860     $csv->parse($line) or do {
861       $dbh->rollback if $oldAutoCommit;
862       return "can't parse: ". $csv->error_input();
863     };
864
865     my @columns = $csv->fields();
866     #warn join('-',@columns);
867
868     if ( $format eq 'simple' ) {
869       @columns = map { s/^ +//; $_; } @columns;
870     }
871
872     my @later = ();
873     my %cdr =
874       map {
875
876         my $field_or_sub = $_;
877         if ( ref($field_or_sub) ) {
878           push @later, $field_or_sub, shift(@columns);
879           ();
880         } else {
881           ( $field_or_sub => shift @columns );
882         }
883
884       }
885       @{ $import_formats{$format} }
886     ;
887
888     my $cdr = new FS::cdr ( \%cdr );
889
890     while ( scalar(@later) ) {
891       my $sub = shift @later;
892       my $data = shift @later;
893       &{$sub}($cdr, $data);  # $cdr->&{$sub}($data); 
894     }
895
896     if ( $format eq 'taqua' ) {
897       if ( $cdr->enddate && $cdr->startdate  ) { #a bit more?
898         $cdr->duration( $cdr->enddate - $cdr->startdate  );
899       }
900       if ( $cdr->enddate && $cdr->answerdate ) { #a bit more?
901         $cdr->billsec(  $cdr->enddate - $cdr->answerdate );
902       } 
903     }
904
905     my $error = $cdr->insert;
906     if ( $error ) {
907       $dbh->rollback if $oldAutoCommit;
908       return $error;
909
910       #or just skip?
911       #next;
912     }
913
914     $imported++;
915   }
916
917   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
918
919   #might want to disable this if we skip records for any reason...
920   return "Empty file!" unless $imported;
921
922   '';
923
924 }
925
926 =back
927
928 =head1 BUGS
929
930 =head1 SEE ALSO
931
932 L<FS::Record>, schema.html from the base documentation.
933
934 =cut
935
936 1;
937