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> |