use strict;
use vars qw( $DEBUG %info );
-use List::Util qw(first min);
use Tie::IxHash;
use Date::Format;
use Text::CSV_XS;
use FS::Conf;
use FS::Record qw(qsearchs qsearch);
use FS::cdr;
+use FS::detail_format;
#use FS::rate;
#use FS::rate_prefix;
#use FS::rate_detail;
'svc_phone.phonenum' => 'Phone numbers (svc_phone.phonenum)',
'svc_pbx.title' => 'PBX name (svc_pbx.title)',
'svc_pbx.svcnum' => 'Freeside service # (svc_pbx.svcnum)',
+ 'svc_pbx.ip.src' => 'PBX name to source IP address',
+ 'svc_pbx.ip.dst' => 'PBX name to destination IP address',
;
tie my %rating_method, 'Tie::IxHash',
2 => 'Flag for later review',
;
+tie my %detail_formats, 'Tie::IxHash',
+ '' => '',
+ FS::cdr::invoice_formats()
+;
+
%info = (
'name' => 'VoIP rating by plan of CDR records in an internal (or external) SQL table',
'shortname' => 'VoIP/telco CDR rating (standard)',
},
'cdr_svc_method' => { 'name' => 'CDR service matching method',
- 'type' => 'radio',
- 'options' => \%cdr_svc_method,
+# 'type' => 'radio',
+ 'type' => 'select',
+ 'select_options' => \%cdr_svc_method,
},
'rating_method' => { 'name' => 'Rating method',
'empty_label' => '',
},
- 'min_included' => { 'name' => 'Minutes included when using the "single price per minute" rating method or when using the "prefix" rating method ("region group" billing)',
+ 'calls_included' => { 'name' => 'Number of calls included at no usage charge', },
+
+ 'min_included' => { 'name' => 'Minutes included when using the "single price per minute" or "prefix" rating method',
},
'min_charge' => { 'name' => 'Charge per minute when using "single price per minute" rating method',
'type' => 'checkbox',
},
- 'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to: ',
+ 'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values: ',
},
'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to: ',
},
#false laziness w/cdr_termination.pm
- 'output_format' => { 'name' => 'CDR invoice display format',
+ 'output_format' => { 'name' => 'CDR display format for invoices',
'type' => 'select',
- 'select_options' => { FS::cdr::invoice_formats() },
+ 'select_options' => \%detail_formats,
'default' => 'default', #XXX test
},
+ 'selfservice_format' =>
+ { 'name' => 'CDR display format for selfservice',
+ 'type' => 'select',
+ 'select_options' => \%detail_formats,
+ 'default' => 'default'
+ },
+ 'selfservice_inbound_format' =>
+ { 'name' => 'Inbound CDR display format for selfservice',
+ 'type' => 'select',
+ 'select_options' => \%detail_formats,
+ 'default' => ''
+ },
+
'usage_section' => { 'name' => 'Section in which to place usage charges (whether separated or not): ',
},
},
#eofalse
+ 'usage_nozero' => { 'name' => 'Omit details for included / no-charge calls.',
+ 'type' => 'checkbox',
+ },
+
'bill_every_call' => { 'name' => 'Generate an invoice immediately for every call (as well any setup fee, upon first payment). Useful for prepaid.',
'type' => 'checkbox',
},
qw(
cdr_svc_method
rating_method ratenum intrastate_ratenum
+ calls_included
min_charge min_included sec_granularity
ignore_unrateable
default_prefix
skip_max_callers
use_duration
411_rewrite
- output_format usage_mandate summarize_usage usage_section
+ output_format
+ selfservice_format selfservice_inbound_format
+ usage_mandate summarize_usage usage_section
bill_every_call bill_inactive_svcs
count_available_phones suspend_bill
)
}
-sub calc_cancel {
- my $self = shift;
- my($cust_pkg, $sdate, $details, $param ) = @_;
-
- $self->calc_usage(@_);
-}
+# use the default
+#sub calc_cancel {
+# my $self = shift;
+# my($cust_pkg, $sdate, $details, $param ) = @_;
+#
+# $self->calc_usage(@_);
+#}
#false laziness w/voip_sqlradacct calc_recur resolve it if that one ever gets used again
my $charges = 0;
my $included_min = $self->option('min_included', 1) || 0; #single price rating
+ my $included_calls = $self->option('calls_included', 1) || 0;
my $cdr_svc_method = $self->option('cdr_svc_method',1)||'svc_phone.phonenum';
my $rating_method = $self->option('rating_method') || 'prefix';
: 'default'
);
- my $use_duration = $self->option('use_duration');
+ my $usage_nozero = $self->option('usage_nozero', 1);
+
+ my $formatter = FS::detail_format->new($output_format, buffer => $details);
- my $csv = new Text::CSV_XS;
+ my $use_duration = $self->option('use_duration');
- my($svc_table, $svc_field) = split('\.', $cdr_svc_method);
+ my($svc_table, $svc_field, $by_ip_addr) = split('\.', $cdr_svc_method);
my @cust_svc;
if( $self->option('bill_inactive_svcs',1) ) {
'status' => '',
'for_update' => 1,
); # $last_bill, $$sdate )
- $options{'by_svcnum'} = 1 if $svc_field eq 'svcnum';
+ if ( $svc_field eq 'svcnum' ) {
+ $options{'by_svcnum'} = 1;
+ }
+ elsif ($svc_table eq 'svc_pbx' and $svc_field eq 'ip') {
+ $options{'by_ip_addr'} = $by_ip_addr;
+ }
- my @invoice_details_sort;
+ #my @invoice_details_sort;
#first rate any outstanding CDRs not yet rated
- foreach my $cdr (
- $svc_x->get_cdrs( %options )
- ) {
+ my $cdr_search = $svc_x->psearch_cdrs(%options);
+ $cdr_search->limit(1000);
+ $cdr_search->increment(0); # because we're changing their status as we go
+ while ( my $cdr = $cdr_search->fetch ) {
my $error = $cdr->rate(
'part_pkg' => $self,
+ 'cust_pkg' => $cust_pkg,
'svcnum' => $svc_x->svcnum,
'single_price_included_min' => \$included_min,
'region_group_included_min' => \$region_group_included_min,
);
die $error if $error; #??
+ $cdr_search->adjust(1) if $cdr->freesidestatus eq '';
+ # it was skipped without changing status, so increment the
+ # offset so that we don't re-fetch it on refill
+
} # $cdr
#then add details to invoices & get a total
$options{'status'} = 'rated';
- foreach my $cdr (
- $svc_x->get_cdrs( %options )
- ) {
-
- my $classnum = '';
- my @call_details = ();
-
- if ( $rating_method eq 'prefix' ) {
-
- $classnum = $cdr->rated_classnum;
-
- unless ( $self->sum_usage ) {
- @call_details = ($cdr->downstream_csv(
- 'format' => $output_format,
- 'granularity' => $cdr->rated_granularity,
- 'seconds' =>($use_duration ? $cdr->duration : $cdr->billsec),
- 'charge' => $cdr->rated_price,
- 'pretty_dst' => $cdr->rated_pretty_dst,
- 'dst_regionname' => $cdr->rated_regionname,
- ));
- }
-
-
- } elsif ( $rating_method eq 'upstream_simple' ) {
-
- $classnum = $cdr->calltypenum; #? meaningful these days?
-
- @call_details = ($cdr->downstream_csv(
- 'format' => $output_format,
- 'charge' => $cdr->rated_price,
- ));
-
- } elsif ( $rating_method eq 'single_price' ) {
- my $granularity = length($self->option_cacheable('sec_granularity'))
- ? $self->option_cacheable('sec_granularity')
- : 60;
-
- @call_details = ($cdr->downstream_csv(
- 'format' => $output_format,
- 'charge' => $cdr->rated_price,
- 'seconds' => ($use_duration ? $cdr->duration : $cdr->billsec),
- 'granularity' => $granularity,
- ));
-
- } else {
- die "don't know how to rate CDRs using method: $rating_method\n";
+ $cdr_search = $svc_x->psearch_cdrs(%options);
+ $cdr_search->limit(1000);
+ $cdr_search->increment(0);
+ while ( my $cdr = $cdr_search->fetch ) {
+ my $error;
+ # at this point we officially Do Not Care about the rating method
+ if ( $included_calls > 0 ) {
+ $included_calls--;
+ #$charges += 0, obviously
+ #but don't set the rated price to zero--there should be a record
+ $error = $cdr->set_status('no-charge');
}
-
- $charges += $cdr->rated_price;
-
- #if ( $cdr->rated_price > 0 ) {
- # generate a detail record for every call; filter out
- # $cdr->rated_price == 0 # later.
- my $call_details;
- my $phonenum = $svc_x->phonenum;
-
- if ( scalar(@call_details) == 1 ) {
- $call_details =
- { format => 'C',
- detail => $call_details[0],
- amount => $cdr->rated_price,
- classnum => $classnum,
- phonenum => $phonenum,
- accountcode => $cdr->accountcode,
- startdate => $cdr->startdate,
- duration => $cdr->rated_seconds,
- regionname => $cdr->rated_regionname,
- };
- } else { #only used for $rating_method eq 'upstream' now
- # and for sum_ formats
- $csv->combine(@call_details);
- $call_details =
- { format => 'C',
- detail => $csv->string,
- amount => $cdr->rated_price,
- classnum => $classnum,
- phonenum => $phonenum,
- accountcode => $cdr->accountcode,
- startdate => $cdr->startdate,
- duration => $cdr->rated_seconds,
- regionname => $cdr->rated_regionname,
- };
+ else {
+ $charges += $cdr->rated_price;
+ $error = $cdr->set_status('done');
}
- $call_details->{'ratename'} = $cdr->rated_ratename;
-
- push @invoice_details_sort, [ $call_details, $cdr->calldate_unix ];
- #} $charge > 0
-
- my $error = $cdr->set_status('done');
- die $error if $error; #??
+ die $error if $error;
+ $formatter->append($cdr) unless $usage_nozero && $cdr->rated_price == 0;
- }
-
- if ( !$self->sum_usage ) {
- #sort them
- my @sorted_invoice_details =
- sort { @{$a}[1] <=> @{$b}[1] } @invoice_details_sort;
- foreach my $sorted_call_detail ( @sorted_invoice_details ) {
- my $d = $sorted_call_detail->[0];
- push @$details, $d if $d->{amount} > 0;
- }
- }
- else { #$self->sum_usage
- push @$details, $self->sum_detail($svc_x, \@invoice_details_sort);
- }
- } # $cust_svc
+ $cdr_search->adjust(1) if $cdr->freesidestatus eq 'rated';
+ } #$cdr
+ }
- unshift @$details, { format => 'C',
- detail => FS::cdr::invoice_header($output_format),
- }
- if @$details && $rating_method ne 'upstream';
+ $formatter->finish; #writes into $details
+ unshift @$details, $formatter->header if @$details;
$charges;
}
return 'amaflags != 2'
if $self->option_cacheable('use_amaflags') && $cdr->amaflags != 2;
- return "disposition NOT IN ( $self->option_cacheable('disposition_in') )"
+ return "disposition NOT IN ( ". $self->option_cacheable('disposition_in')." )"
if $self->option_cacheable('disposition_in') =~ /\S/
&& !grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $self->option_cacheable('disposition_in'));
- return "disposition IN ( $self->option_cacheable('ignore_disposition') )"
+ return "disposition IN ( ". $self->option_cacheable('ignore_disposition')." )"
if $self->option_cacheable('ignore_disposition') =~ /\S/
&& grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $self->option_cacheable('ignore_disposition'));
if length($_) && substr($cdr->dst,0,length($_)) eq $_;
}
- return "carrierid != $self->option_cacheable('use_carrierid')"
- if length($self->option_cacheable('use_carrierid'))
- && $cdr->carrierid ne $self->option_cacheable('use_carrierid') #ne otherwise 0 matches ''
- && ! $flags{'da_rewrote'};
+ return "carrierid NOT IN ( ". $self->option_cacheable('use_carrierid'). " )"
+ if $self->option_cacheable('use_carrierid') =~ /\S/
+ && ! $flags{'da_rewrote'} #why?
+ && !grep { $cdr->carrierid eq $_ } split(/\s*,\s*/, $self->option_cacheable('use_carrierid')); #eq otherwise 0 matches ''
# unlike everything else, use_cdrtypenum is applied in FS::svc_x::get_cdrs.
- return "cdrtypenum != $self->option_cacheable('use_cdrtypenum')"
+ return "cdrtypenum != ". $self->option_cacheable('use_cdrtypenum')
if length($self->option_cacheable('use_cdrtypenum'))
&& $cdr->cdrtypenum ne $self->option_cacheable('use_cdrtypenum'); #ne otherwise 0 matches ''
- return "cdrtypenum == $self->option_cacheable('ignore_cdrtypenum')"
+ return "cdrtypenum == ". $self->option_cacheable('ignore_cdrtypenum')
if length($self->option_cacheable('ignore_cdrtypenum'))
&& $cdr->cdrtypenum eq $self->option_cacheable('ignore_cdrtypenum'); #eq otherwise 0 matches ''
- return "dcontext IN ( $self->option_cacheable('skip_dcontext') )"
+ return "dcontext IN ( ". $self->option_cacheable('skip_dcontext'). " )"
if $self->option_cacheable('skip_dcontext') =~ /\S/
&& grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext'));
my $len_prefix = length($self->option_cacheable('skip_dstchannel_prefix'));
- return "dstchannel starts with $self->option_cacheable('skip_dstchannel_prefix')"
+ return "dstchannel starts with ". $self->option_cacheable('skip_dstchannel_prefix')
if $len_prefix
&& substr($cdr->dstchannel,0,$len_prefix) eq $self->option_cacheable('skip_dstchannel_prefix');
&& $cdr->is_tollfree('accountcode')
);
- return "lastapp is $self->option_cacheable('skip_lastapp')"
+ return "lastapp is ". $self->option_cacheable('skip_lastapp')
if length($self->option_cacheable('skip_lastapp')) && $cdr->lastapp eq $self->option_cacheable('skip_lastapp');
my $src_length = $self->option_cacheable('skip_src_length_more');
$count;
}
+sub reset_usage {
+ my ($self, $cust_pkg, %opt) = @_;
+ my @part_pkg_usage = $self->part_pkg_usage or return '';
+ warn " resetting usage minutes\n" if $opt{debug};
+ my %cust_pkg_usage = map { $_->pkgusagepart, $_ } $cust_pkg->cust_pkg_usage;
+ foreach my $part_pkg_usage (@part_pkg_usage) {
+ my $part = $part_pkg_usage->pkgusagepart;
+ my $usage = $cust_pkg_usage{$part} ||
+ FS::cust_pkg_usage->new({
+ 'pkgnum' => $cust_pkg->pkgnum,
+ 'pkgusagepart' => $part,
+ 'minutes' => $part_pkg_usage->minutes,
+ });
+ foreach my $cdr_usage (
+ qsearch('cdr_cust_pkg_usage', {'cdrusagenum' => $usage->cdrusagenum})
+ ) {
+ my $error = $cdr_usage->delete;
+ warn " error resetting CDR usage: $error\n";
+ }
+
+ if ( $usage->pkgusagenum ) {
+ if ( $part_pkg_usage->rollover ) {
+ $usage->set('minutes', $part_pkg_usage->minutes + $usage->minutes);
+ } else {
+ $usage->set('minutes', $part_pkg_usage->minutes);
+ }
+ my $error = $usage->replace;
+ warn " error resetting usage minutes: $error\n" if $error;
+ } else {
+ my $error = $usage->insert;
+ warn " error resetting usage minutes: $error\n" if $error;
+ }
+ } #foreach $part_pkg_usage
+}
+
# tells whether cust_bill_pkg_detail should return a single line for
# each phonenum
sub sum_usage {
$self->option('output_format') =~ /^sum_/;
}
-sub sum_detail {
- my $self = shift;
- my $svc_x = shift;
- my $invoice_details = shift || [];
- return () if !@$invoice_details;
- my $details_by_rate = {};
- # combine the entire set of CDRs
- foreach ( @$invoice_details ) {
- my $d = $_->[0];
- my $sum = $details_by_rate->{ $d->{ratename} } ||= {
- amount => 0,
- format => 'C',
- classnum => '', #XXX
- duration => 0,
- phonenum => $svc_x->phonenum,
- accountcode => '', #XXX
- startdate => '', #XXX
- regionname => '',
- count => 0,
- };
- $sum->{amount} += $d->{amount};
- $sum->{duration} += $d->{duration};
- $sum->{count}++;
- }
- my @details;
- foreach my $ratename ( sort keys(%$details_by_rate) ) {
- my $sum = $details_by_rate->{$ratename};
- next if $sum->{count} == 0;
- my $total_cdr = FS::cdr->new({
- 'billsec' => $sum->{duration},
- 'src' => $sum->{phonenum},
- });
- $sum->{detail} = $total_cdr->downstream_csv(
- format => $self->option('output_format'),
- seconds => $sum->{duration},
- charge => sprintf('%.2f',$sum->{amount}),
- ratename => $ratename,
- phonenum => $sum->{phonenum},
- count => $sum->{count},
- );
- push @details, $sum;
- }
- @details;
-}
-
# and whether cust_bill should show a detail line for the service label
# (separate from usage details)
sub hide_svc_detail {