diff options
| -rw-r--r-- | FS/FS/Schema.pm | 26 | ||||
| -rw-r--r-- | FS/FS/cdr.pm | 242 | ||||
| -rw-r--r-- | FS/FS/cust_bill.pm | 25 | ||||
| -rw-r--r-- | FS/FS/cust_bill_pkg.pm | 57 | ||||
| -rw-r--r-- | FS/FS/cust_bill_pkg_detail.pm | 1 | ||||
| -rw-r--r-- | FS/FS/part_pkg/voip_cdr.pm | 251 | ||||
| -rw-r--r-- | conf/invoice_html | 16 | ||||
| -rw-r--r-- | conf/invoice_latex | 13 | ||||
| -rw-r--r-- | httemplate/misc/cdr-import.html | 14 | 
9 files changed, 502 insertions, 143 deletions
| diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 11e63ea71..e72cfa9a1 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -417,6 +417,7 @@ sub tables_hashref {          'detailnum', 'serial', '', '', '', '',           'pkgnum',  'int', '', '', '', '',           'invnum',  'int', '', '', '', '',  +        'format',  'char', 'NULL', 1, '', '',          'detail',  'varchar', '', $char_d, '', '',         ],        'primary_key' => 'detailnum', @@ -1775,6 +1776,31 @@ sub tables_hashref {        'index'  => [],      }, +    'report' => { +      'columns' => [ +        'reportnum', 'serial',   '',      '', '', '', +        'usernum',   'int',      '',      '', '', '', +        'public',    'char', 'NULL',       1, '', '',  +        'menu',      'char', 'NULL',       1, '', '',  +        'name',      'varchar',  '', $char_d, '', '', +      ], +      'primary_key' => 'reportnum', +      'unique' => [], +      'index'  => [ [ 'usernum' ] ], +    }, + +    'report_option' => { +      'columns' => [ +        'optionnum',   'serial',     '',      '', '', '',  +        'reportnum',      'int',     '',      '', '', '',  +        'optionname', 'varchar',     '', $char_d, '', '',  +        'optionvalue',   'text', 'NULL',      '', '', '',  +      ], +      'primary_key' => 'optionnum', +      'unique'      => [], +      'index'       => [ [ 'reportnum' ], [ 'optionname' ] ], +    }, +      'svc_phone' => {        'columns' => [          'svcnum',      'int',         '',      '', '', '',  diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm index 5078ff610..29bbe0e99 100644 --- a/FS/FS/cdr.pm +++ b/FS/FS/cdr.pm @@ -408,6 +408,14 @@ my %export_formats = (      sub { shift->rated_price ? 'Y' : 'N' }, #RATED      '', #OTHER_INFO    ], +  'voxlinesystems' => [ +    sub { time2str('%D', shift->calldate_unix ) },   #DATE +    sub { time2str('%T', shift->calldate_unix ) },   #TIME +    'userfield',                                     #USER +    'dst',                                           #NUMBER_DIALED +    sub { sprintf('%.2fm', shift->billsec / 60 ) },  #DURATION +    sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE +  ],  );  sub downstream_csv { @@ -440,17 +448,32 @@ sub downstream_csv {  =over 4 -=item batch_import +=item import_formats + +Returns an ordered list of key value pairs containing import format names +as keys (for use with batch_import) and "pretty" format names as values.  =cut +sub import_formats { +  ( +    'asterisk'       => 'Asterisk', +    'taqua'          => 'Taqua', +    'unitel'         => 'Unitel/RSLCOM', +    'voxlinesystems' => 'VoxLineSystems',  #XXX? get the actual vendor name +    'simple'         => 'Simple', +  ); +} +  my($tmp_mday, $tmp_mon, $tmp_year);  sub _cdr_date_parser_maker {    my $field = shift;    return sub {      my( $cdr, $date ) = @_; -    $cdr->$field( _cdr_date_parse($date) ); +    #$cdr->$field( _cdr_date_parse($date) ); +    eval { $cdr->$field( _cdr_date_parse($date) ); }; +    die "error parsing date for $field from $date: $@\n" if $@;    };  } @@ -460,13 +483,18 @@ sub _cdr_date_parse {    return '' unless length($date); #that's okay, it becomes NULL    #$date =~ /^\s*(\d{4})[\-\/]\(\d{1,2})[\-\/](\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s*$/ -  $date =~ /^\s*(\d{4})\D(\d{1,2})\D(\d{1,2})\s+(\d{1,2})\D(\d{1,2})\D(\d{1,2})\s*$/ +  $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|$)/      or die "unparsable date: $date"; #maybe we shouldn't die...    my($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 ); +  return '' if $year == 1900 && $mon == 1 && $day == 1 +            && $hour == 0    && $min == 0 && $sec == 0; +    timelocal($sec, $min, $hour, $day, $mon-1, $year);  } +#taqua  #2007-10-31 08:57:24.113000000 +  #http://www.the-asterisk-book.com/unstable/funktionen-cdr.html  my %amaflags = (    DEFAULT       => 0, @@ -499,6 +527,130 @@ my %import_formats = (      'uniqueid',      'userfield',    ], +  'taqua' => [ #some of these are kind arbitrary... + +    sub { my($cdr, $field) = @_; },       #XXX interesting RecordType +             # easy to fix: Can't find cdr.cdrtypenum 1 in cdr_type.cdrtypenum + +    sub { my($cdr, $field) = @_; },             #all10#RecordVersion +    sub { my($cdr, $field) = @_; },       #OrigShelfNumber +    sub { my($cdr, $field) = @_; },       #OrigCardNumber +    sub { my($cdr, $field) = @_; },       #OrigCircuit +    sub { my($cdr, $field) = @_; },       #OrigCircuitType +    'uniqueid',                           #SequenceNumber +    'accountcode',                        #SessionNumber +    'src',                                #CallingPartyNumber +    'dst',                                #CalledPartyNumber +    _cdr_date_parser_maker('startdate'),  #CallArrivalTime +    _cdr_date_parser_maker('enddate'),    #CallCompletionTime + +    #Disposition +    #sub { my($cdr, $d ) = @_; $cdr->disposition( $disposition{$d}): }, +    'disposition', +                                          #  -1 => '', +                                          #   0 => '', +                                          # 100 => '', +                                          # 101 => '', +                                          # 102 => '', +                                          # 103 => '', +                                          # 104 => '', +                                          # 105 => '', +                                          # 201 => '', +                                          # 203 => '', + +    _cdr_date_parser_maker('answerdate'), #DispositionTime +    sub { my($cdr, $field) = @_; },       #TCAP +    sub { my($cdr, $field) = @_; },       #OutboundCarrierConnectTime +    sub { my($cdr, $field) = @_; },       #OutboundCarrierDisconnectTime + +    #TermTrunkGroup +    #it appears channels are actually part of trunk groups, but this data +    #is interesting and we need a source and destination place to put it +    'dstchannel',                         #TermTrunkGroup + + +    sub { my($cdr, $field) = @_; },       #TermShelfNumber +    sub { my($cdr, $field) = @_; },       #TermCardNumber +    sub { my($cdr, $field) = @_; },       #TermCircuit +    sub { my($cdr, $field) = @_; },       #TermCircuitType +    sub { my($cdr, $field) = @_; },       #OutboundCarrierId +    'charged_party',                      #BillingNumber +    sub { my($cdr, $field) = @_; },       #SubscriberNumber +    'lastapp',                            #ServiceName +    sub { my($cdr, $field) = @_; },       #some weirdness #ChargeTime +    'lastdata',                           #ServiceInformation +    sub { my($cdr, $field) = @_; },       #FacilityInfo +    sub { my($cdr, $field) = @_; },             #all 1900-01-01 0#CallTraceTime +    sub { my($cdr, $field) = @_; },             #all-1#UniqueIndicator +    sub { my($cdr, $field) = @_; },             #all-1#PresentationIndicator +    sub { my($cdr, $field) = @_; },             #empty#Pin +    sub { my($cdr, $field) = @_; },       #CallType +    sub { my($cdr, $field) = @_; },           #Balt/empty #OrigRateCenter +    sub { my($cdr, $field) = @_; },           #Balt/empty #TermRateCenter + +    #OrigTrunkGroup +    #it appears channels are actually part of trunk groups, but this data +    #is interesting and we need a source and destination place to put it +    'channel',                            #OrigTrunkGroup + +    'userfield',                                #empty#UserDefined +    sub { my($cdr, $field) = @_; },             #empty#PseudoDestinationNumber +    sub { my($cdr, $field) = @_; },             #all-1#PseudoCarrierCode +    sub { my($cdr, $field) = @_; },             #empty#PseudoANI +    sub { my($cdr, $field) = @_; },             #all-1#PseudoFacilityInfo +    sub { my($cdr, $field) = @_; },       #OrigDialedDigits +    sub { my($cdr, $field) = @_; },             #all-1#OrigOutboundCarrier +    sub { my($cdr, $field) = @_; },       #IncomingCarrierID +    'dcontext',                           #JurisdictionInfo +    sub { my($cdr, $field) = @_; },       #OrigDestDigits +    sub { my($cdr, $field) = @_; },       #huh?#InsertTime +    sub { my($cdr, $field) = @_; },       #key +    sub { my($cdr, $field) = @_; },             #empty#AMALineNumber +    sub { my($cdr, $field) = @_; },             #empty#AMAslpID +    sub { my($cdr, $field) = @_; },             #empty#AMADigitsDialedWC +    sub { my($cdr, $field) = @_; },       #OpxOffHook +    sub { my($cdr, $field) = @_; },       #OpxOnHook + +        #acctid - primary key +  #AUTO #calldate - Call timestamp (SQL timestamp) +#clid - Caller*ID with text +        #XXX src - Caller*ID number / Source number +        #XXX dst - Destination extension +        #dcontext - Destination context +        #channel - Channel used +        #dstchannel - Destination channel if appropriate +        #lastapp - Last application if appropriate +        #lastdata - Last application data +        #startdate - Start of call (UNIX-style integer timestamp) +        #answerdate - Answer time of call (UNIX-style integer timestamp) +        #enddate - End time of call (UNIX-style integer timestamp) +  #HACK#duration - Total time in system, in seconds +  #HACK#XXX billsec - Total time call is up, in seconds +        #disposition - What happened to the call: ANSWERED, NO ANSWER, BUSY +#INT amaflags - What flags to use: BILL, IGNORE etc, specified on a per channel basis like accountcode. +        #accountcode - CDR account number to use: account + +        #uniqueid - Unique channel identifier (Unitel/RSLCOM Event ID) +        #userfield - CDR user-defined field + +        #X cdrtypenum - CDR type - see FS::cdr_type (Usage = 1, S&E = 7, OC&C = 8) +        #XXX charged_party - Service number to be billed +#upstream_currency - Wholesale currency from upstream +#X upstream_price - Wholesale price from upstream +#upstream_rateplanid - Upstream rate plan ID +#rated_price - Rated (or re-rated) price +#distance - km (need units field?) +#islocal - Local - 1, Non Local = 0 +#calltypenum - Type of call - see FS::cdr_calltype +#X description - Description (cdr_type 7&8 only) (used for cust_bill_pkg.itemdesc) +#quantity - Number of items (cdr_type 7&8 only) +#carrierid - Upstream Carrier ID (see FS::cdr_carrier) +#upstream_rateid - Upstream Rate ID + +        #svcnum - Link to customer service (see FS::cust_svc) +        #freesidestatus - NULL, done (or something) + +  ],    'unitel' => [      'uniqueid',      #'cdr_type', @@ -524,6 +676,50 @@ my %import_formats = (      'carrierid',      'upstream_rateid',    ], +  'voxlinesystems' => [ #XXX get the actual vendor name +    'disposition',                        #Status +    'startdate',                          #Start (what do you know, a timestamp! +    sub { my($cdr, $field) = @_; },       #Start date +    sub { my($cdr, $field) = @_; },       #Start time +    'enddate',                            #End (also a timestamp!) +    sub { my($cdr, $field) = @_; },       #End date +    sub { my($cdr, $field) = @_; },       #End time +    'accountcode',                        #Calling customer XXX map to agent_custid?? +    sub { my($cdr, $field) = @_; },       #Calling type +    sub { shift->src('30000'); }, #XXX FAKE XXX 'src',                                #Calling number +    'userfield',                          #Calling name #? +    sub { my($cdr, $field) = @_; },       #Called type +    'dst',                                #Called number +    sub { my($cdr, $field) = @_; },       #Destination customer +    sub { my($cdr, $field) = @_; },       #Destination type +    sub { my($cdr, $field) = @_; },       #Destination Number +    sub { my($cdr, $field) = @_; },       #Inbound calling type +    sub { my($cdr, $field) = @_; },       #Inbound calling number +    sub { my($cdr, $field) = @_; },       #Inbound called type +    sub { my($cdr, $field) = @_; },       #Inbound called number +    sub { my($cdr, $field) = @_; },       #Inbound destination type +    sub { my($cdr, $field) = @_; },       #Inbound destination number +    sub { my($cdr, $field) = @_; },       #Outbound calling type +    sub { my($cdr, $field) = @_; },       #Outbound calling number +    sub { my($cdr, $field) = @_; },       #Outbound called type +    sub { my($cdr, $field) = @_; },       #Outbound called number +    sub { my($cdr, $field) = @_; },       #Outbound destination type +    sub { my($cdr, $field) = @_; },       #Outbound destination number +    sub { my($cdr, $field) = @_; },       #Internal calling type +    sub { my($cdr, $field) = @_; },       #Internal calling number +    sub { my($cdr, $field) = @_; },       #Internal called type +    sub { my($cdr, $field) = @_; },       #Internal called number +    sub { my($cdr, $field) = @_; },       #Internal destination type +    sub { my($cdr, $field) = @_; },       #Internal destination number +    'duration',                           #Total seconds +    sub { my($cdr, $field) = @_; },       #Ring seconds +    'billsec',                            #Billable seconds +    'upstream_price',                     #Cost +    sub { my($cdr, $field) = @_; },       #Billing customer +    sub { my($cdr, $field) = @_; },       #Billing customer name +    sub { my($cdr, $field) = @_; },       #Billing type +    sub { my($cdr, $field) = @_; },       #Billing reference +  ],    'simple' => [      # Date @@ -561,6 +757,26 @@ my %import_formats = (    ],  ); +my %import_header = ( +  'simple'         => 1, +  'taqua'          => 1, +  'voxlinesystems' => 2, #XXX vendor name +); + +=item batch_import HASHREF + +Imports CDR records.  Available options are: + +=over 4 + +=item filehandle + +=item format + +=back + +=cut +  sub batch_import {    my $param = shift; @@ -588,18 +804,13 @@ sub batch_import {    local $FS::UID::AutoCommit = 0;    my $dbh = dbh; -  if ( $format eq 'simple' ) { # and other formats with a header too? - -  } +  my $header_lines = +    exists($import_header{$format}) ? $import_header{$format} : 0; -  my $body = 0;    my $line;    while ( defined($line=<$fh>) ) { -    #skip header... -    if ( ! $body++ && $format eq 'simple' && $line =~ /^[\w\, ]+$/ ) { -      next; -    } +    next if $header_lines-- > 0; #&& $line =~ /^[\w, "]+$/       $csv->parse($line) or do {        $dbh->rollback if $oldAutoCommit; @@ -637,6 +848,15 @@ sub batch_import {        &{$sub}($cdr, $data);  # $cdr->&{$sub}($data);       } +    if ( $format eq 'taqua' ) { +      if ( $cdr->enddate && $cdr->startdate  ) { #a bit more? +        $cdr->duration( $cdr->enddate - $cdr->startdate  ); +      } +      if ( $cdr->enddate && $cdr->answerdate ) { #a bit more? +        $cdr->billsec(  $cdr->enddate - $cdr->answerdate ); +      }  +    } +      my $error = $cdr->insert;      if ( $error ) {        $dbh->rollback if $oldAutoCommit; diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 94e00bd49..3f5a4ea57 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -1949,7 +1949,8 @@ sub print_latex {      $invoice_data{'detail_items'} = \@detail_items;      $invoice_data{'total_items'} = \@total_items; -    foreach my $line_item ( $self->_items($conf->exists('disable_previous_balance') ? qw( _items_pkg ) : () ) ) { +    my %options = ( 'format' => 'latex', 'escape_function' => \&_latex_escape ); +    foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {        my $detail = {          ext_description => [],        }; @@ -1957,9 +1958,7 @@ sub print_latex {        $detail->{'quantity'} = 1;        $detail->{'description'} = _latex_escape($line_item->{'description'});        if ( exists $line_item->{'ext_description'} ) { -        @{$detail->{'ext_description'}} = map { -          _latex_escape($_); -        } @{$line_item->{'ext_description'}}; +        @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};        }        $detail->{'amount'} = $line_item->{'amount'};        $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; @@ -2266,16 +2265,15 @@ sub print_html {    my $money_char = $conf->config('money_char') || '$'; -  foreach my $line_item ( $self->_items($conf->exists('disable_previous_balance') ? qw( _items_pkg ) : () ) ) { +  my %options = ( 'format' => 'html', 'escape_function' => \&encode_entities ); +  foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {      my $detail = {        ext_description => [],      };      $detail->{'ref'} = $line_item->{'pkgnum'};      $detail->{'description'} = encode_entities($line_item->{'description'});      if ( exists $line_item->{'ext_description'} ) { -      @{$detail->{'ext_description'}} = map { -        encode_entities($_); -      } @{$line_item->{'ext_description'}}; +      @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};      }      $detail->{'amount'} = $money_char. $line_item->{'amount'};      $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; @@ -2439,6 +2437,9 @@ sub _items_tax {  sub _items_cust_bill_pkg {    my $self = shift;    my $cust_bill_pkg = shift; +  my %opt = @_; +  my $format = $opt{format} || ''; +  my $escape_function = $opt{escape_function} || sub { shift };    my @b = ();    foreach my $cust_bill_pkg ( @$cust_bill_pkg ) { @@ -2453,7 +2454,10 @@ sub _items_cust_bill_pkg {          my $description = $desc;          $description .= ' Setup' if $cust_bill_pkg->recur != 0;          my @d = $cust_pkg->h_labels_short($self->_date); -        push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0; +        push @d, $cust_bill_pkg->details( 'format'          => $format, +                                          'escape_function' => $escape_function, +                                        ) +          if $cust_bill_pkg->recur == 0;          push @b, {            description     => $description,            #pkgpart         => $part_pkg->pkgpart, @@ -2480,7 +2484,8 @@ sub _items_cust_bill_pkg {              [ $cust_pkg->h_labels_short( $self->_date ),                                           #$cust_bill_pkg->edate,                                           #$cust_bill_pkg->sdate), -              $cust_bill_pkg->details, +              $cust_bill_pkg->details( 'format'          => $format, +                                       'escape_function' => $escape_function),              ],          };        } diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 9fddf6bf5..a0a211bb0 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -110,7 +110,8 @@ sub insert {      my $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail {        'pkgnum' => $self->pkgnum,        'invnum' => $self->invnum, -      'detail' => $detail, +      'format' => (ref($detail) ? $detail->[0] : '' ), +      'detail' => (ref($detail) ? $detail->[1] : $detail ),      };      $error = $cust_bill_pkg_detail->insert;      if ( $error ) { @@ -203,18 +204,62 @@ sub cust_bill {    qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );  } -=item details +=item details [ OPTION => VALUE ... ]  Returns an array of detail information for the invoice line item. +Currently available options are: I<format> I<escape_function> + +If I<format> is set to html or latex then the array members are improved +for tabular appearance in those environments if possible. + +If I<escape_function> is set then the array members are processed by this +function before being returned. +  =cut  sub details { -  my $self = shift; +  my ( $self, %opt ) = @_; +  my $format = $opt{format} || ''; +  my $escape_function = $opt{escape_function} || sub { shift };    return () unless defined dbdef->table('cust_bill_pkg_detail'); -  map { $_->detail } -    qsearch ( 'cust_bill_pkg_detail', { 'pkgnum' => $self->pkgnum, -                                        'invnum' => $self->invnum, } ); + +  eval "use Text::CSV_XS;"; +  die $@ if $@; +  my $csv = new Text::CSV_XS; + +  my $format_sub = sub { my $detail = shift; +                         $csv->parse($detail) or return "can't parse $detail"; +                         join(' - ', map { &$escape_function($_) } +                                     $csv->fields +                             ); +                       }; + +  $format_sub = sub { my $detail = shift; +                      $csv->parse($detail) or return "can't parse $detail"; +                      join('</TD><TD>', map { &$escape_function($_) } +                                        $csv->fields +                          ); +                    } +    if $format eq 'html'; + +  $format_sub = sub { my $detail = shift; +                      $csv->parse($detail) or return "can't parse $detail"; +                      join(' & ', map { &$escape_function($_) } $csv->fields ); +                    } +    if $format eq 'latex'; + +  map { ( $_->format eq 'C' +          ? &{$format_sub}( $_->detail ) +          : &{$escape_function}( $_->detail ) +        ) +      } +    qsearch ({ 'table'    => 'cust_bill_pkg_detail', +               'hashref'  => { 'pkgnum' => $self->pkgnum, +                               'invnum' => $self->invnum, +                             }, +               'order_by' => 'ORDER BY detailnum', +            });      #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });  } diff --git a/FS/FS/cust_bill_pkg_detail.pm b/FS/FS/cust_bill_pkg_detail.pm index 4156816c8..a69998a42 100644 --- a/FS/FS/cust_bill_pkg_detail.pm +++ b/FS/FS/cust_bill_pkg_detail.pm @@ -104,6 +104,7 @@ sub check {    $self->ut_numbern('detailnum')      || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum')      || $self->ut_foreign_key('invnum', 'cust_bill', 'invnum') +    || $self->ut_enum('format', [ '', 'C' ] )      || $self->ut_text('detail')      || $self->SUPER::check      ; diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm index 72ef55ff0..00691e30a 100644 --- a/FS/FS/part_pkg/voip_cdr.pm +++ b/FS/FS/part_pkg/voip_cdr.pm @@ -17,6 +17,7 @@ $DEBUG = 1;  tie my %rating_method, 'Tie::IxHash',    'prefix' => 'Rate calls by using destination prefix to look up a region and rate according to the internal prefix and rate tables',    'upstream' => 'Rate calls based on upstream data: If the call type is "1", map the upstream rate ID directly to an internal rate (rate_detail), otherwise, pass the upstream price through directly.', +  'upstream_simple' => 'Simply pass through and charge the "upstream_price" amount.',  ;  #tie my %cdr_location, 'Tie::IxHash', @@ -69,6 +70,14 @@ tie my %rating_method, 'Tie::IxHash',                                  'default' => '011',                                }, +    'use_amaflags' => { 'name' => 'Do not charge for CDRs where the amaflags field is not set to "2" ("BILL"/"BILLING").', +                        'type' => 'checkbox', +                      }, + +    'use_disposition' => { 'name' => 'Do not charge for CDRs where the disposition flag is not set to "ANSWERED".', +                           'type' => 'checkbox', +                         }, +      #XXX also have option for an external db  #    'cdr_location' => { 'name' => 'CDR database location'  #                        'type' => 'select', @@ -93,7 +102,15 @@ tie my %rating_method, 'Tie::IxHash',  #                  },    }, -  'fieldorder' => [qw( setup_fee recur_flat unused_credit ratenum rating_method default_prefix disable_src domestic_prefix international_prefix )], +  'fieldorder' => [qw( +                       setup_fee recur_flat unused_credit +                       rating_method ratenum  +                       default_prefix +                       disable_src +                       domestic_prefix international_prefix +                       use_amaflags use_disposition +                     ) +                  ],    'weight' => 40,  ); @@ -133,94 +150,109 @@ sub calc_recur {        my $rate_detail;        my( $rate_region, $regionnum );        my $pretty_destnum; -      my $charge = 0; +      my $charge = '';        my @call_details = ();        if ( $self->option('rating_method') eq 'prefix'             || ! $self->option('rating_method')           )        { -        ### -        # look up rate details based on called station id -        # (or calling station id for toll free calls) -        ### - -        my( $to_or_from, $number ); -        if ( $cdr->dst =~ /^(\+?1)?8([02-8])\1/ ) { #tollfree call -          $to_or_from = 'from'; -          $number = $cdr->src; -        } else { #regular call -          $to_or_from = 'to'; -          $number = $cdr->dst; -        } -   -        #remove non-phone# stuff and whitespace -        $number =~ s/\s//g; -#        my $proto = ''; -#        $dest =~ s/^(\w+):// and $proto = $1; #sip: -#        my $siphost = ''; -#        $dest =~ s/\@(.*)$// and $siphost = $1; # @10.54.32.1, @sip.example.com - -        my $intl = $self->option('international_prefix') || '011'; -   -        #determine the country code -        my $countrycode; -        if (    $number =~ /^$intl(((\d)(\d))(\d))(\d+)$/ -             || $number =~ /^\+(((\d)(\d))(\d))(\d+)$/ -           ) -        { -   -          my( $three, $two, $one, $u1, $u2, $rest ) = ( $1,$2,$3,$4,$5,$6 ); -          #first look for 1 digit country code -          if ( qsearch('rate_prefix', { 'countrycode' => $one } ) ) { -            $countrycode = $one; -            $number = $u1.$u2.$rest; -          } elsif ( qsearch('rate_prefix', { 'countrycode' => $two } ) ) { #or 2 -            $countrycode = $two; -            $number = $u2.$rest; -          } else { #3 digit country code -            $countrycode = $three; -            $number = $rest; -          } -   +        if ( $self->option('use_amaflags') && $cdr->amaflags != 2 ) { + +          warn "not charging for CDR (amaflags != 2)\n" if $DEBUG; +          $charge = 0; + +        } elsif ( $self->option('use_disposition') +                  && $cdr->disposition ne 'ANSWERED' ) { + +          warn "not charging for CDR (disposition != ANSWERED)\n" if $DEBUG; +          $charge = 0; +          } else { -          $countrycode = $self->option('domestic_prefix') || '1'; -          $number =~ s/^$countrycode//;# if length($number) > 10; -        } -   -        warn "rating call $to_or_from +$countrycode $number\n" if $DEBUG; -        $pretty_destnum = "+$countrycode $number"; -   -        #find a rate prefix, first look at most specific (4 digits) then 3, etc., -        # finally trying the country code only -        my $rate_prefix = ''; -        for my $len ( reverse(1..6) ) { -          $rate_prefix = qsearchs('rate_prefix', { + +          ### +          # look up rate details based on called station id +          # (or calling station id for toll free calls) +          ### + +          my( $to_or_from, $number ); +          if ( $cdr->dst =~ /^(\+?1)?8([02-8])\1/ ) { #tollfree call +            $to_or_from = 'from'; +            $number = $cdr->src; +          } else { #regular call +            $to_or_from = 'to'; +            $number = $cdr->dst; +          } + +          #remove non-phone# stuff and whitespace +          $number =~ s/\s//g; +#          my $proto = ''; +#          $dest =~ s/^(\w+):// and $proto = $1; #sip: +#          my $siphost = ''; +#          $dest =~ s/\@(.*)$// and $siphost = $1; # @10.54.32.1, @sip.example.com + +          my $intl = $self->option('international_prefix') || '011'; + +          #determine the country code +          my $countrycode; +          if (    $number =~ /^$intl(((\d)(\d))(\d))(\d+)$/ +               || $number =~ /^\+(((\d)(\d))(\d))(\d+)$/ +             ) +          { + +            my( $three, $two, $one, $u1, $u2, $rest ) = ( $1,$2,$3,$4,$5,$6 ); +            #first look for 1 digit country code +            if ( qsearch('rate_prefix', { 'countrycode' => $one } ) ) { +              $countrycode = $one; +              $number = $u1.$u2.$rest; +            } elsif ( qsearch('rate_prefix', { 'countrycode' => $two } ) ) { #or 2 +              $countrycode = $two; +              $number = $u2.$rest; +            } else { #3 digit country code +              $countrycode = $three; +              $number = $rest; +            } + +          } else { +            $countrycode = $self->option('domestic_prefix') || '1'; +            $number =~ s/^$countrycode//;# if length($number) > 10; +          } + +          warn "rating call $to_or_from +$countrycode $number\n" if $DEBUG; +          $pretty_destnum = "+$countrycode $number"; + +          #find a rate prefix, first look at most specific (4 digits) then 3, etc., +          # finally trying the country code only +          my $rate_prefix = ''; +          for my $len ( reverse(1..6) ) { +            $rate_prefix = qsearchs('rate_prefix', { +              'countrycode' => $countrycode, +              #'npa'         => { op=> 'LIKE', value=> substr($number, 0, $len) } +              'npa'         => substr($number, 0, $len), +            } ) and last; +          } +          $rate_prefix ||= qsearchs('rate_prefix', {              'countrycode' => $countrycode, -            #'npa'         => { op=> 'LIKE', value=> substr($number, 0, $len) } -            'npa'         => substr($number, 0, $len), -          } ) and last; +            'npa'         => '', +          }); + +          # +          die "Can't find rate for call $to_or_from +$countrycode $\numbern" +            unless $rate_prefix; + +          $regionnum = $rate_prefix->regionnum; +          $rate_detail = qsearchs('rate_detail', { +            'ratenum'        => $ratenum, +            'dest_regionnum' => $regionnum, +          } ); + +          $rate_region = $rate_prefix->rate_region; + +          warn "  found rate for regionnum $regionnum ". +               "and rate detail $rate_detail\n" +            if $DEBUG; +          } -        $rate_prefix ||= qsearchs('rate_prefix', { -          'countrycode' => $countrycode, -          'npa'         => '', -        }); - -        # -        die "Can't find rate for call $to_or_from +$countrycode $\numbern" -          unless $rate_prefix; -   -        $regionnum = $rate_prefix->regionnum; -        $rate_detail = qsearchs('rate_detail', { -          'ratenum'        => $ratenum, -          'dest_regionnum' => $regionnum, -        } ); -   -        $rate_region = $rate_prefix->rate_region; - -        warn "  found rate for regionnum $regionnum ". -             "and rate detail $rate_detail\n" -          if $DEBUG;        } elsif ( $self->option('rating_method') eq 'upstream' ) { @@ -240,7 +272,8 @@ sub calc_recur {          } else { #pass upstream price through            $charge = sprintf('%.2f', $cdr->upstream_price); -   +          $charges += $charge; +            @call_details = (              #time2str("%Y %b %d - %r", $cdr->calldate_unix ),              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 @@ -252,6 +285,14 @@ sub calc_recur {          } +      } elsif ( $self->option('rating_method') eq 'upstream_simple' ) { + +        #XXX $charge = sprintf('%.2f', $cdr->upstream_price); +        $charge = sprintf('%.3f', $cdr->upstream_price); +        $charges += $charge; + +        @call_details = ( $cdr->downstream_csv( 'format' => 'voxlinesystems' )); +        } else {          die "don't know how to rate CDRs using method: ".              $self->option('rating_method'). "\n"; @@ -265,18 +306,19 @@ sub calc_recur {        # don't add it to invoice, don't set its status to NULL,        # don't call downstream_csv or something on it...        # but DO emit a warning... -      if ( ! $rate_detail && ! scalar(@call_details) ) { -   +      #if ( ! $rate_detail && ! scalar(@call_details) ) { +      if ( ! $rate_detail && $charge eq '' ) { +          warn "no rate_detail found for CDR.acctid:  ". $cdr->acctid.               "; skipping\n"        } else { # there *is* a rate_detail (or call_details), proceed... -        unless ( @call_details ) { -     +        unless ( @call_details || ( $charge ne '' && $charge == 0 ) ) { +            $included_min{$regionnum} = $rate_detail->min_included              unless exists $included_min{$regionnum}; -       +            my $granularity = $rate_detail->sec_granularity;            my $seconds = $cdr->billsec; # length($cdr->billsec) ? $cdr->billsec : $cdr->duration;            $seconds += $granularity - ( $seconds % $granularity ) @@ -287,19 +329,19 @@ sub calc_recur {            # per call rather than per minute            $minutes = 1 unless $granularity; -       +            $included_min{$regionnum} -= $minutes; -       +            if ( $included_min{$regionnum} < 0 ) {              my $charge_min = 0 - $included_min{$regionnum};              $included_min{$regionnum} = 0;              $charge = sprintf('%.2f', $rate_detail->min_charge * $charge_min );              $charges += $charge;            } -       +            # this is why we need regionnum/rate_region....            warn "  (rate region $rate_region)\n" if $DEBUG; -       +            @call_details = (              #time2str("%Y %b %d - %r", $cdr->calldate_unix ),              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 @@ -310,26 +352,35 @@ sub calc_recur {            );          } -     -        warn "  adding details on charge to invoice: ". -             join(' - ', @call_details ) -          if $DEBUG; -     -        push @$details, join(' - ', @call_details); #\@call_details, -   + +        if ( $charge > 0 ) { +          my $call_details; +          if ( $self->option('rating_method') eq 'upstream_simple' ) { +            $call_details = [ 'C', $call_details[0] ]; +          }else{ +            $call_details = join(' - ', @call_details ); +          } +          warn "  adding details on charge to invoice: $call_details" +            if $DEBUG; +          push @$details, $call_details; #\@call_details, +        } +          # if the customer flag is on, call "downstream_csv" or something          # like it to export the call downstream!          # XXX price plan option to pick format, or something...          $downstream_cdr .= $cdr->downstream_csv( 'format' => 'convergent' )            if $spool_cdr; -   +          my $error = $cdr->set_status_and_rated_price('done', $charge);          die $error if $error; -   +        } -   +      } # $cdr +    unshift @$details, [ 'C', "Date,Time,Name,Destination,Duration,Price" ] +      if (@$details && $self->option('rating_method') eq 'upstream_simple' ); +    } # $cust_svc    if ( $spool_cdr && length($downstream_cdr) ) { diff --git a/conf/invoice_html b/conf/invoice_html index b13b08f46..ddede78ce 100644 --- a/conf/invoice_html +++ b/conf/invoice_html @@ -86,13 +86,15 @@              '<td align="right">'. $line->{'amount'}. '</td>'.            '</tr>'          ; -        foreach my $ext_desc ( @{$line->{'ext_description'} } ) { -          $OUT .= -            '<tr class="invoice_extdesc">'. -              '<td></td>'. -              '<td align="left">- '. $ext_desc. '</td>'. -              '<td></td>'. -            '</tr>' +        if ( @{$line->{'ext_description'} } ) { +          $OUT .= '<tr class="invoice_extdesc"><td></td><td><table>'; +          foreach my $ext_desc ( @{$line->{'ext_description'} } ) { +            $OUT .= +              '<tr class="invoice_extdesc">'. +                '<td align="left">- '. $ext_desc. '</td>'. +              '</tr>' +          } +          $OUT .= '</table></td><td></td></tr>';          }        } diff --git a/conf/invoice_latex b/conf/invoice_latex index c63514568..ba66d6437 100644 --- a/conf/invoice_latex +++ b/conf/invoice_latex @@ -229,10 +229,15 @@ Terms: [@-- $terms --@]\\      $OUT .= '\FSdesc{' . $line->{'ref'} . '}{' . $line->{'description'} . '}' .
              '{' . $line->{'amount'} . "}${rowbreak}\n";
 -    foreach my $ext_desc (@$ext_description) {
 -      $ext_desc = substr($ext_desc, 0, 80) . '...'
 -        if (length($ext_desc) > 80);
 -      $OUT .= '\FSextdesc{' . $ext_desc . '}' . "${rowbreak}\n";
 +    if (@$ext_description) {
 +      $OUT .= '\multicolumn{1}{l}{\rule{0pt}{1.0ex}} &';
 +      $OUT .= '\multicolumn{2}{l}{\small{\begin{tabular}{llllll}';#cheating at 6
 +      foreach my $ext_desc (@$ext_description) {
 +        $ext_desc = substr($ext_desc, 0, 80) . '...'
 +          if (length($ext_desc) > 80);
 +        $OUT .= "$ext_desc \\\\${rowbreak}\n";
 +      }
 +      $OUT .="\\end{tabular}}}\\\\${rowbreak}\n";
      }
    }
 diff --git a/httemplate/misc/cdr-import.html b/httemplate/misc/cdr-import.html index 60f619e81..b71a3e365 100644 --- a/httemplate/misc/cdr-import.html +++ b/httemplate/misc/cdr-import.html @@ -1,11 +1,13 @@  <% include("/elements/header.html",'Call Detail Record Import') %>  <FORM ACTION="process/cdr-import.html" METHOD="POST" ENCTYPE="multipart/form-data">  Import a CSV file containing Call Detail Records (CDRs).<BR><BR> -CDR Format: <SELECT NAME="format"> -<OPTION VALUE="asterisk">Asterisk</OPTION> -<OPTION VALUE="unitel">Unitel/RSLCOM</OPTION> -<OPTION VALUE="simple">Simple</OPTION> -</SELECT><BR><BR> +CDR Format: +<SELECT NAME="format"> +% foreach my $format ( keys %formats ) { +  <OPTION VALUE="<% $format %>"><% $formats{$format} %></OPTION> +% } +</SELECT> +<BR><BR>  Filename: <INPUT TYPE="file" NAME="csvfile"><BR><BR> @@ -19,4 +21,6 @@ Filename: <INPUT TYPE="file" NAME="csvfile"><BR><BR>  die "access denied"    unless $FS::CurrentUser::CurrentUser->access_right('Import'); +tie my %formats, 'Tie::IxHash', FS::cdr->import_formats; +  </%init> | 
