remove name from voxlinesystems2, really
[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,Called From,Destination,Duration,Price",
404                            "Date,Time,Called From,Destination,Duration,Price",
405                        },
406 );
407
408 my %export_formats = (
409   'convergent' => [
410     'carriername', #CARRIER
411     sub { shift->_convergent_format('src') }, #SERVICE_NUMBER
412     sub { shift->_convergent_format('charged_party') }, #CHARGED_NUMBER
413     sub { time2str('%Y-%m-%d', shift->calldate_unix ) }, #DATE
414     sub { time2str('%T',       shift->calldate_unix ) }, #TIME
415     'billsec', #'duration', #DURATION
416     sub { shift->_convergent_format('dst') }, #NUMBER_DIALED
417     '', #XXX add (from prefixes in most recent email) #FROM_DESC
418     '', #XXX add (from prefixes in most recent email) #TO_DESC
419     'calltypename', #CLASS_CODE
420     'rated_price', #PRICE
421     sub { shift->rated_price ? 'Y' : 'N' }, #RATED
422     '', #OTHER_INFO
423   ],
424   'voxlinesystems' => [
425     sub { time2str('%D', shift->calldate_unix ) },   #DATE
426     sub { time2str('%r', shift->calldate_unix ) },   #TIME
427     'userfield',                                     #USER
428     'dst',                                           #NUMBER_DIALED
429     sub { sprintf('%.2fm', shift->billsec / 60 ) },  #DURATION
430     sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
431   ],
432   'voxlinesystems2' => [
433     sub { time2str('%D', shift->calldate_unix ) },   #DATE
434     sub { time2str('%r', shift->calldate_unix ) },   #TIME
435     #'userfield',                                     #USER
436     'dst',                                           #NUMBER_DIALED
437     'src',                                           #called from
438     sub { sprintf('%.2fm', shift->billsec / 60 ) },  #DURATION
439     sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
440   ],
441 );
442
443 sub downstream_csv {
444   my( $self, %opt ) = @_;
445
446   my $format = $opt{'format'}; # 'convergent';
447   return "Unknown format $format" unless exists $export_formats{$format};
448
449   eval "use Text::CSV_XS;";
450   die $@ if $@;
451   my $csv = new Text::CSV_XS;
452
453   my @columns =
454     map {
455           ref($_) ? &{$_}($self) : $self->$_();
456         }
457     @{ $export_formats{$format} };
458
459   my $status = $csv->combine(@columns);
460   die "FS::CDR: error combining ". $csv->error_input(). "into downstream CSV"
461     unless $status;
462
463   $csv->string;
464
465 }
466
467 =back
468
469 =head1 CLASS METHODS
470
471 =over 4
472
473 =item invoice_formats
474
475 Returns an ordered list of key value pairs containing invoice format names
476 as keys (for use with part_pkg::voip_cdr) and "pretty" format names as values.
477
478 =cut
479
480 sub invoice_formats {
481   map { ($_ => $export_names{$_}->{'name'}) }
482     grep { $export_names{$_}->{'invoice_header'} }
483     keys %export_names;
484 }
485
486 =item invoice_header FORMAT
487
488 Returns a scalar containing the CSV column header for invoice format FORMAT.
489
490 =cut
491
492 sub invoice_header {
493   my $format = shift;
494   $export_names{$format}->{'invoice_header'};
495 }
496
497 =item import_formats
498
499 Returns an ordered list of key value pairs containing import format names
500 as keys (for use with batch_import) and "pretty" format names as values.
501
502 =cut
503
504 sub import_formats {
505   (
506     'asterisk'       => 'Asterisk',
507     'taqua'          => 'Taqua',
508     'unitel'         => 'Unitel/RSLCOM',
509     'voxlinesystems' => 'VoxLineSystems',  #XXX? get the actual vendor name
510     'simple'         => 'Simple',
511   );
512 }
513
514 my($tmp_mday, $tmp_mon, $tmp_year);
515
516 sub _cdr_date_parser_maker {
517   my $field = shift;
518   return sub {
519     my( $cdr, $date ) = @_;
520     #$cdr->$field( _cdr_date_parse($date) );
521     eval { $cdr->$field( _cdr_date_parse($date) ); };
522     die "error parsing date for $field from $date: $@\n" if $@;
523   };
524 }
525
526 sub _cdr_date_parse {
527   my $date = shift;
528
529   return '' unless length($date); #that's okay, it becomes NULL
530
531   #$date =~ /^\s*(\d{4})[\-\/]\(\d{1,2})[\-\/](\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s*$/
532   $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|$)/
533     or die "unparsable date: $date"; #maybe we shouldn't die...
534   my($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
535
536   return '' if $year == 1900 && $mon == 1 && $day == 1
537             && $hour == 0    && $min == 0 && $sec == 0;
538
539   timelocal($sec, $min, $hour, $day, $mon-1, $year);
540 }
541
542 #taqua  #2007-10-31 08:57:24.113000000
543
544 #http://www.the-asterisk-book.com/unstable/funktionen-cdr.html
545 my %amaflags = (
546   DEFAULT       => 0,
547   OMIT          => 1, #asterisk 1.4+
548   IGNORE        => 1, #asterisk 1.2
549   BILLING       => 2, #asterisk 1.4+
550   BILL          => 2, #asterisk 1.2
551   DOCUMENTATION => 3,
552   #? '' => 0,
553 );
554
555 my %import_formats = (
556   'asterisk' => [
557     'accountcode',
558     'src',
559     'dst',
560     'dcontext',
561     'clid',
562     'channel',
563     'dstchannel',
564     'lastapp',
565     'lastdata',
566     _cdr_date_parser_maker('startdate'),
567     _cdr_date_parser_maker('answerdate'),
568     _cdr_date_parser_maker('enddate'),
569     'duration',
570     'billsec',
571     'disposition',
572     sub { my($cdr, $amaflags) = @_; $cdr->amaflags($amaflags{$amaflags}); },
573     'uniqueid',
574     'userfield',
575   ],
576   'taqua' => [ #some of these are kind arbitrary...
577
578     sub { my($cdr, $field) = @_; },       #XXX interesting RecordType
579              # easy to fix: Can't find cdr.cdrtypenum 1 in cdr_type.cdrtypenum
580
581     sub { my($cdr, $field) = @_; },             #all10#RecordVersion
582     sub { my($cdr, $field) = @_; },       #OrigShelfNumber
583     sub { my($cdr, $field) = @_; },       #OrigCardNumber
584     sub { my($cdr, $field) = @_; },       #OrigCircuit
585     sub { my($cdr, $field) = @_; },       #OrigCircuitType
586     'uniqueid',                           #SequenceNumber
587     'accountcode',                        #SessionNumber
588     'src',                                #CallingPartyNumber
589     'dst',                                #CalledPartyNumber
590     _cdr_date_parser_maker('startdate'),  #CallArrivalTime
591     _cdr_date_parser_maker('enddate'),    #CallCompletionTime
592
593     #Disposition
594     #sub { my($cdr, $d ) = @_; $cdr->disposition( $disposition{$d}): },
595     'disposition',
596                                           #  -1 => '',
597                                           #   0 => '',
598                                           # 100 => '',
599                                           # 101 => '',
600                                           # 102 => '',
601                                           # 103 => '',
602                                           # 104 => '',
603                                           # 105 => '',
604                                           # 201 => '',
605                                           # 203 => '',
606
607     _cdr_date_parser_maker('answerdate'), #DispositionTime
608     sub { my($cdr, $field) = @_; },       #TCAP
609     sub { my($cdr, $field) = @_; },       #OutboundCarrierConnectTime
610     sub { my($cdr, $field) = @_; },       #OutboundCarrierDisconnectTime
611
612     #TermTrunkGroup
613     #it appears channels are actually part of trunk groups, but this data
614     #is interesting and we need a source and destination place to put it
615     'dstchannel',                         #TermTrunkGroup
616
617
618     sub { my($cdr, $field) = @_; },       #TermShelfNumber
619     sub { my($cdr, $field) = @_; },       #TermCardNumber
620     sub { my($cdr, $field) = @_; },       #TermCircuit
621     sub { my($cdr, $field) = @_; },       #TermCircuitType
622     sub { my($cdr, $field) = @_; },       #OutboundCarrierId
623     'charged_party',                      #BillingNumber
624     sub { my($cdr, $field) = @_; },       #SubscriberNumber
625     'lastapp',                            #ServiceName
626     sub { my($cdr, $field) = @_; },       #some weirdness #ChargeTime
627     'lastdata',                           #ServiceInformation
628     sub { my($cdr, $field) = @_; },       #FacilityInfo
629     sub { my($cdr, $field) = @_; },             #all 1900-01-01 0#CallTraceTime
630     sub { my($cdr, $field) = @_; },             #all-1#UniqueIndicator
631     sub { my($cdr, $field) = @_; },             #all-1#PresentationIndicator
632     sub { my($cdr, $field) = @_; },             #empty#Pin
633     sub { my($cdr, $field) = @_; },       #CallType
634     sub { my($cdr, $field) = @_; },           #Balt/empty #OrigRateCenter
635     sub { my($cdr, $field) = @_; },           #Balt/empty #TermRateCenter
636
637     #OrigTrunkGroup
638     #it appears channels are actually part of trunk groups, but this data
639     #is interesting and we need a source and destination place to put it
640     'channel',                            #OrigTrunkGroup
641
642     'userfield',                                #empty#UserDefined
643     sub { my($cdr, $field) = @_; },             #empty#PseudoDestinationNumber
644     sub { my($cdr, $field) = @_; },             #all-1#PseudoCarrierCode
645     sub { my($cdr, $field) = @_; },             #empty#PseudoANI
646     sub { my($cdr, $field) = @_; },             #all-1#PseudoFacilityInfo
647     sub { my($cdr, $field) = @_; },       #OrigDialedDigits
648     sub { my($cdr, $field) = @_; },             #all-1#OrigOutboundCarrier
649     sub { my($cdr, $field) = @_; },       #IncomingCarrierID
650     'dcontext',                           #JurisdictionInfo
651     sub { my($cdr, $field) = @_; },       #OrigDestDigits
652     sub { my($cdr, $field) = @_; },       #huh?#InsertTime
653     sub { my($cdr, $field) = @_; },       #key
654     sub { my($cdr, $field) = @_; },             #empty#AMALineNumber
655     sub { my($cdr, $field) = @_; },             #empty#AMAslpID
656     sub { my($cdr, $field) = @_; },             #empty#AMADigitsDialedWC
657     sub { my($cdr, $field) = @_; },       #OpxOffHook
658     sub { my($cdr, $field) = @_; },       #OpxOnHook
659
660         #acctid - primary key
661   #AUTO #calldate - Call timestamp (SQL timestamp)
662 #clid - Caller*ID with text
663         #XXX src - Caller*ID number / Source number
664         #XXX dst - Destination extension
665         #dcontext - Destination context
666         #channel - Channel used
667         #dstchannel - Destination channel if appropriate
668         #lastapp - Last application if appropriate
669         #lastdata - Last application data
670         #startdate - Start of call (UNIX-style integer timestamp)
671         #answerdate - Answer time of call (UNIX-style integer timestamp)
672         #enddate - End time of call (UNIX-style integer timestamp)
673   #HACK#duration - Total time in system, in seconds
674   #HACK#XXX billsec - Total time call is up, in seconds
675         #disposition - What happened to the call: ANSWERED, NO ANSWER, BUSY
676 #INT amaflags - What flags to use: BILL, IGNORE etc, specified on a per channel basis like accountcode.
677         #accountcode - CDR account number to use: account
678
679         #uniqueid - Unique channel identifier (Unitel/RSLCOM Event ID)
680         #userfield - CDR user-defined field
681
682         #X cdrtypenum - CDR type - see FS::cdr_type (Usage = 1, S&E = 7, OC&C = 8)
683         #XXX charged_party - Service number to be billed
684 #upstream_currency - Wholesale currency from upstream
685 #X upstream_price - Wholesale price from upstream
686 #upstream_rateplanid - Upstream rate plan ID
687 #rated_price - Rated (or re-rated) price
688 #distance - km (need units field?)
689 #islocal - Local - 1, Non Local = 0
690 #calltypenum - Type of call - see FS::cdr_calltype
691 #X description - Description (cdr_type 7&8 only) (used for cust_bill_pkg.itemdesc)
692 #quantity - Number of items (cdr_type 7&8 only)
693 #carrierid - Upstream Carrier ID (see FS::cdr_carrier)
694 #upstream_rateid - Upstream Rate ID
695
696         #svcnum - Link to customer service (see FS::cust_svc)
697         #freesidestatus - NULL, done (or something)
698
699   ],
700   'unitel' => [
701     'uniqueid',
702     #'cdr_type',
703     'cdrtypenum',
704     'calldate', # may need massaging?  huh maybe not...
705     #'billsec', #XXX duration and billsec?
706                 sub { $_[0]->billsec(  $_[1] );
707                       $_[0]->duration( $_[1] );
708                     },
709     'src',
710     'dst', # XXX needs to have "+61" prepended unless /^\+/ ???
711     'charged_party',
712     'upstream_currency',
713     'upstream_price',
714     'upstream_rateplanid',
715     'distance',
716     'islocal',
717     'calltypenum',
718     'startdate',  #XXX needs massaging
719     'enddate',    #XXX same
720     'description',
721     'quantity',
722     'carrierid',
723     'upstream_rateid',
724   ],
725   'voxlinesystems' => [ #XXX get the actual vendor name
726     'disposition',                        #Status
727     'startdate',                          #Start (what do you know, a timestamp!
728     sub { my($cdr, $field) = @_; },       #Start date
729     sub { my($cdr, $field) = @_; },       #Start time
730     'enddate',                            #End (also a timestamp!)
731     sub { my($cdr, $field) = @_; },       #End date
732     sub { my($cdr, $field) = @_; },       #End time
733     'accountcode',                        #Calling customer... map to agent_custid??
734     sub { my($cdr, $field) = @_; },       #Calling type
735     'src',
736     #sub { my($cdr, $field) = @_; },       #Calling number
737     'userfield',                          #Calling name #?
738     sub { my($cdr, $field) = @_; },       #Called type
739     'dst',                                #Called number
740     sub { my($cdr, $field) = @_; },       #Destination customer
741     sub { my($cdr, $field) = @_; },       #Destination type
742     sub { my($cdr, $field) = @_; },       #Destination Number
743     sub { my($cdr, $field) = @_; },       #Inbound calling type
744     sub { my($cdr, $field) = @_; },       #Inbound calling number
745     #'src',
746     sub { my($cdr, $field) = @_; },       #Inbound called type
747     sub { my($cdr, $field) = @_; },       #Inbound called number
748     sub { my($cdr, $field) = @_; },       #Inbound destination type
749     sub { my($cdr, $field) = @_; },       #Inbound destination number
750     sub { my($cdr, $field) = @_; },       #Outbound calling type
751     sub { my($cdr, $field) = @_; },       #Outbound calling number
752     sub { my($cdr, $field) = @_; },       #Outbound called type
753     sub { my($cdr, $field) = @_; },       #Outbound called number
754     sub { my($cdr, $field) = @_; },       #Outbound destination type
755     sub { my($cdr, $field) = @_; },       #Outbound destination number
756     sub { my($cdr, $field) = @_; },       #Internal calling type
757     sub { my($cdr, $field) = @_; },       #Internal calling number
758     sub { my($cdr, $field) = @_; },       #Internal called type
759     sub { my($cdr, $field) = @_; },       #Internal called number
760     sub { my($cdr, $field) = @_; },       #Internal destination type
761     sub { my($cdr, $field) = @_; },       #Internal destination number
762     'duration',                           #Total seconds
763     sub { my($cdr, $field) = @_; },       #Ring seconds
764     'billsec',                            #Billable seconds
765     'upstream_price',                     #Cost
766     sub { my($cdr, $field) = @_; },       #Billing customer
767     sub { my($cdr, $field) = @_; },       #Billing customer name
768     sub { my($cdr, $field) = @_; },       #Billing type
769     sub { my($cdr, $field) = @_; },       #Billing reference
770   ],
771   'simple' => [
772
773     # Date
774     sub { my($cdr, $date) = @_;
775           $date =~ /^(\d{1,2})\/(\d{1,2})\/(\d\d(\d\d)?)$/
776             or die "unparsable date: $date"; #maybe we shouldn't die...
777           #$cdr->startdate( timelocal(0, 0, 0 ,$2, $1-1, $3) );
778           ($tmp_mday, $tmp_mon, $tmp_year) = ( $2, $1-1, $3 );
779         },
780
781     # Time
782     sub { my($cdr, $time) = @_;
783           #my($sec, $min, $hour, $mday, $mon, $year)= localtime($cdr->startdate);
784           $time =~ /^(\d{1,2}):(\d{1,2}):(\d{1,2})$/
785             or die "unparsable time: $time"; #maybe we shouldn't die...
786           #$cdr->startdate( timelocal($3, $2, $1 ,$mday, $mon, $year) );
787           $cdr->startdate(
788             timelocal($3, $2, $1 ,$tmp_mday, $tmp_mon, $tmp_year)
789           );
790         },
791
792     # Source_Number
793     'src',
794
795     # Terminating_Number
796     'dst',
797
798     # Duration
799     sub { my($cdr, $min) = @_;
800           my $sec = sprintf('%.0f', $min * 60 );
801           $cdr->billsec(  $sec );
802           $cdr->duration( $sec );
803         },
804
805   ],
806 );
807
808 my %import_header = (
809   'simple'         => 1,
810   'taqua'          => 1,
811   'voxlinesystems' => 2, #XXX vendor name
812 );
813
814 =item batch_import HASHREF
815
816 Imports CDR records.  Available options are:
817
818 =over 4
819
820 =item filehandle
821
822 =item format
823
824 =back
825
826 =cut
827
828 sub batch_import {
829   my $param = shift;
830
831   my $fh = $param->{filehandle};
832   my $format = $param->{format};
833
834   return "Unknown format $format" unless exists $import_formats{$format};
835
836   eval "use Text::CSV_XS;";
837   die $@ if $@;
838
839   my $csv = new Text::CSV_XS;
840
841   my $imported = 0;
842   #my $columns;
843
844   local $SIG{HUP} = 'IGNORE';
845   local $SIG{INT} = 'IGNORE';
846   local $SIG{QUIT} = 'IGNORE';
847   local $SIG{TERM} = 'IGNORE';
848   local $SIG{TSTP} = 'IGNORE';
849   local $SIG{PIPE} = 'IGNORE';
850
851   my $oldAutoCommit = $FS::UID::AutoCommit;
852   local $FS::UID::AutoCommit = 0;
853   my $dbh = dbh;
854
855   my $header_lines =
856     exists($import_header{$format}) ? $import_header{$format} : 0;
857
858   my $line;
859   while ( defined($line=<$fh>) ) {
860
861     next if $header_lines-- > 0; #&& $line =~ /^[\w, "]+$/ 
862
863     $csv->parse($line) or do {
864       $dbh->rollback if $oldAutoCommit;
865       return "can't parse: ". $csv->error_input();
866     };
867
868     my @columns = $csv->fields();
869     #warn join('-',@columns);
870
871     if ( $format eq 'simple' ) {
872       @columns = map { s/^ +//; $_; } @columns;
873     }
874
875     my @later = ();
876     my %cdr =
877       map {
878
879         my $field_or_sub = $_;
880         if ( ref($field_or_sub) ) {
881           push @later, $field_or_sub, shift(@columns);
882           ();
883         } else {
884           ( $field_or_sub => shift @columns );
885         }
886
887       }
888       @{ $import_formats{$format} }
889     ;
890
891     my $cdr = new FS::cdr ( \%cdr );
892
893     while ( scalar(@later) ) {
894       my $sub = shift @later;
895       my $data = shift @later;
896       &{$sub}($cdr, $data);  # $cdr->&{$sub}($data); 
897     }
898
899     if ( $format eq 'taqua' ) {
900       if ( $cdr->enddate && $cdr->startdate  ) { #a bit more?
901         $cdr->duration( $cdr->enddate - $cdr->startdate  );
902       }
903       if ( $cdr->enddate && $cdr->answerdate ) { #a bit more?
904         $cdr->billsec(  $cdr->enddate - $cdr->answerdate );
905       } 
906     }
907
908     my $error = $cdr->insert;
909     if ( $error ) {
910       $dbh->rollback if $oldAutoCommit;
911       return $error;
912
913       #or just skip?
914       #next;
915     }
916
917     $imported++;
918   }
919
920   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
921
922   #might want to disable this if we skip records for any reason...
923   return "Empty file!" unless $imported;
924
925   '';
926
927 }
928
929 =back
930
931 =head1 BUGS
932
933 =head1 SEE ALSO
934
935 L<FS::Record>, schema.html from the base documentation.
936
937 =cut
938
939 1;
940