use strict;
use vars qw( $DEBUG %info );
use Tie::IxHash;
+use Date::Parse;
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;
$DEBUG = 0;
'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',
+ 'svc_acct.username' => 'Username (svc_acct.username)',
;
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.',
- 'single_price' => 'A single price per minute for all calls.',
+ 'upstream_simple' => 'Simply pass through and charge the "upstream_price" amount (ignoring all options to skip calls).',
+ 'single_price' => 'A single price per minute for all calls (ignoring all options to skip calls).',
+;
+
+tie my %rounding, 'Tie::IxHash',
+ '2' => 'Two decimal places (cent)',
+ '4' => 'Four decimal places (100th of a cent)',
;
#tie my %cdr_location, 'Tie::IxHash',
2 => 'Flag for later review',
;
+tie my %detail_formats, 'Tie::IxHash',
+ '' => '',
+ FS::cdr::invoice_formats()
+;
+
+tie my %accountcode_tollfree_field, 'Tie::IxHash',
+ 'dst' => 'Destination (dst)',
+ 'src' => 'Source (src)',
+;
+
%info = (
- 'name' => 'VoIP rating by plan of CDR records in an internal (or external) SQL table',
+ 'name' => 'VoIP rating by plan of CDR records',
'shortname' => 'VoIP/telco CDR rating (standard)',
'inherit_fields' => [ 'prorate_Mixin', 'global_Mixin' ],
'fields' => {
'options' => \%rating_method,
},
+ 'rounding' => { 'name' => 'Rounding for destination prefix rating',
+ 'type' => 'select',
+ 'select_options' => \%rounding,
+ },
+
'ratenum' => { 'name' => 'Rate plan',
- 'type' => 'select',
- 'select_table' => 'rate',
- 'select_key' => 'ratenum',
- 'select_label' => 'ratename',
+ 'type' => 'select-rate',
},
'intrastate_ratenum' => { 'name' => 'Optional alternate intrastate rate plan',
- 'type' => 'select',
- 'select_table' => 'rate',
- 'select_key' => 'ratenum',
- 'select_label' => 'ratename',
+ 'type' => 'select-rate',
'disable_empty' => 0,
- 'empty_label' => '',
+ 'empty_label' => ' ',
},
'calls_included' => { 'name' => 'Number of calls included at no usage charge', },
# 'type' => 'checkbox',
# },
- 'international_prefix' => { 'name' => 'Destination prefix for international CDR records',
+ 'international_prefix' => { 'name' => 'Destination prefix for international CDR records (or "none" for no prefix)',
'default' => '011',
},
'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: ',
+ 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to this cdrtypenum: ',
+ 'validate' => \&FS::part_pkg::validate_number_blank,
+ 'js_validate' => 'digits: true',
},
- 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to: ',
+ 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to this cdrtypenum: ',
+ 'validate' => \&FS::part_pkg::validate_number_blank,
+ 'js_validate' => 'digits: true',
+ },
+
+ 'use_calltypenum' => { 'name' => 'Only charge for CDRs where the CDR Call Type is set to this calltypenum: ',
+ },
+
+ 'ignore_calltypenum' => { 'name' => 'Do not charge for CDRs where the CDR Call Type is set to this calltypenum: ',
},
'ignore_disposition' => { 'name' => 'Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values: ',
'disposition_in' => { 'name' => 'Only charge for CDRs where the Disposition is set to any of these (comma-separated) values: ',
},
- 'skip_dst_prefix' => { 'name' => 'Do not charge for CDRs where the destination number starts with any of these values: ',
+ 'disposition_prefix' => { 'name' => 'Only charge for CDRs where the Disposition starts with: ',
+ },
+
+ 'skip_dst_prefix' => { 'name' => 'Do not charge for CDRs where the destination number starts with any of these (comma-separated) values: ',
},
- 'skip_dcontext' => { 'name' => 'Do not charge for CDRs where the dcontext is set to any of these (comma-separated) values: ',
+ 'skip_dcontext' => { 'name' => 'Do not charge for CDRs where dcontext is set to any of these (comma-separated) values: ',
+ },
+
+ 'skip_dcontext_prefix' => { 'name' => 'Do not charge for CDRs where dcontext starts with: ',
+ },
+
+ 'skip_dcontext_suffix' => { 'name' => 'Do not charge for CDRs where dcontext ends with: ',
},
'skip_dstchannel_prefix' => { 'name' => 'Do not charge for CDRs where the dstchannel starts with:',
'empty_label' => '',
},
+ 'accountcode_tollfree_field' => {
+ 'name' => 'When using an alternate rate plan for toll-free accountcodes, the CDR field to use in rating calculations',
+ 'type' => 'select',
+ 'select_options' => \%accountcode_tollfree_field,
+ },
+
'skip_dst_length_less' => { 'name' => 'Do not charge for CDRs where the destination is less than this many digits:',
},
'skip_max_callers' => { 'name' => 'Do not charge for CDRs where max_callers is less than or equal to this value: ',
},
+ 'skip_same_customer' => {
+ 'name' => 'Do not charge for calls between numbers belonging to the same customer',
+ 'type' => 'checkbox',
+ },
+
'use_duration' => { 'name' => 'Calculate usage based on the duration field instead of the billsec field',
'type' => 'checkbox',
},
},
#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): ',
},
'type' => 'checkbox',
},
- 'usage_mandate' => { 'name' => 'Always put usage details in separate section',
+ 'usage_mandate' => { 'name' => 'Always put usage details in separate section. The section is defined in the next option.',
'type' => 'checkbox',
},
#eofalse
+ 'usage_showzero' => { 'name' => 'Show 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',
},
'bill_inactive_svcs' => { 'name' => 'Bill for all phone numbers that were active during the billing period',
'type' => 'checkbox',
},
-
- 'count_available_phones' => { 'name' => 'Consider for tax purposes the number of lines to be svc_phones that may be provisioned rather than those that actually are.',
- 'type' => 'checkbox',
- },
+ 'bill_only_pkg_dates' => { 'name' => 'Only bill CDRs with a date during the package billing period',
+ 'type' => 'checkbox',
+ },
+ 'skip_old' => { 'name' => 'Do not charge for CDRs older than: ',
+ 'type' => 'date',
+ },
#XXX also have option for an external db? these days we suck them into ours
# 'cdr_location' => { 'name' => 'CDR database location'
FS::part_pkg::prorate_Mixin::fieldorder,
qw(
cdr_svc_method
- rating_method ratenum intrastate_ratenum
+ rating_method rounding ratenum intrastate_ratenum
calls_included
- min_charge min_included sec_granularity
+ min_charge min_included
+ sec_granularity
ignore_unrateable
default_prefix
disable_src
use_amaflags
use_carrierid
use_cdrtypenum ignore_cdrtypenum
- ignore_disposition disposition_in
- skip_dcontext skip_dst_prefix
+ use_calltypenum ignore_calltypenum
+ ignore_disposition disposition_in disposition_prefix
+ skip_dcontext skip_dcontext_prefix skip_dcontext_suffix
+ skip_dst_prefix
skip_dstchannel_prefix skip_src_length_more
noskip_src_length_accountcode_tollfree
- accountcode_tollfree_ratenum
+ accountcode_tollfree_ratenum accountcode_tollfree_field
skip_dst_length_less
noskip_dst_length_accountcode_tollfree
skip_lastapp
skip_max_callers
+ skip_same_customer
use_duration
411_rewrite
- output_format usage_mandate summarize_usage usage_section
- bill_every_call bill_inactive_svcs
- count_available_phones suspend_bill
+ output_format
+ selfservice_format selfservice_inbound_format
+ usage_mandate usage_section summarize_usage
+ usage_showzero bill_every_call bill_inactive_svcs
+ bill_only_pkg_dates skip_old
+ suspend_bill
)
],
- 'weight' => 40,
+ 'weight' => 41,
+ 'validate' => sub {
+ # Validation function for FS::part_pkg::check_options()
+ my $options = shift;
+ return "Please choose a Rate Plan for use with selected Rating Method"
+ if $options->{rating_method} eq 'prefix' && !$options->{ratenum};
+ return;
+ },
);
sub price_info {
my $self = shift;
- my $str = $self->SUPER::price_info;
+ my $str = $self->SUPER::price_info(@_);
$str .= " plus usage" if $str;
$str;
}
my $charges = 0;
$charges += $self->calc_usage(@_);
- $charges += $self->calc_recur_Common(@_);
+ $charges += ($cust_pkg->quantity || 1) * $self->calc_recur_Common(@_);
$charges;
my $charges = 0;
- my $included_min = $self->option('min_included', 1) || 0; #single price rating
+ my $included_min_total = ($self->option('min_included', 1) || 0)
+ * ($cust_pkg->quantity || 1);
+ #single price rating
+ #or region group
+ my $included_min_left = $included_min_total;
+
my $included_calls = $self->option('calls_included', 1) || 0;
+ $included_calls *= ($cust_pkg->quantity || 1);
my $cdr_svc_method = $self->option('cdr_svc_method',1)||'svc_phone.phonenum';
my $rating_method = $self->option('rating_method') || 'prefix';
- my $region_group_included_min = $self->option('min_included',1) || 0;
- my %region_group_included_min = ();
+ my %detail_included_min = ();
my $output_format = $self->option('output_format', 'Hush!')
|| ( $rating_method eq 'upstream_simple'
: 'default'
);
- my $formatter = FS::detail_format->new($output_format, buffer => $details);
+ my $usage_showzero = $self->option('usage_showzero', 1);
+
+ my $formatter = FS::detail_format->new($output_format,
+ buffer => $details,
+ locale => $cust_pkg->cust_main->locale,
+ rounding => ($self->option_cacheable('rounding') || 2),
+ );
my $use_duration = $self->option('use_duration');
$svc_x = $cust_svc->svc_x;
}
+ unless ( $svc_x ) {
+ my $h = $self->option('bill_inactive_svcs',1) ? 'h_' : '';
+ warn "WARNING: no $h$svc_table for svcnum ". $cust_svc->svcnum. "\n";
+ }
+
my %options = (
- 'disable_src' => $self->option('disable_src'),
- 'default_prefix' => $self->option('default_prefix'),
- 'cdrtypenum' => $self->option('use_cdrtypenum'),
+ 'disable_src' => $self->option('disable_src',1),
+ 'default_prefix' => $self->option('default_prefix',1),
+ 'cdrtypenum' => $self->option('use_cdrtypenum',1),
+ 'calltypenum' => $self->option('use_calltypenum',1),
'status' => '',
'for_update' => 1,
- ); # $last_bill, $$sdate )
+ );
+ if ( $self->option('bill_only_pkg_dates') ) {
+ $options{'begin'} = $last_bill;
+ $options{'end'} = $$sdate;
+ }
if ( $svc_field eq 'svcnum' ) {
$options{'by_svcnum'} = 1;
- }
- elsif ($svc_table eq 'svc_pbx' and $svc_field eq 'ip') {
+ } elsif ($svc_table eq 'svc_pbx' and $svc_field eq 'ip') {
$options{'by_ip_addr'} = $by_ip_addr;
}
#my @invoice_details_sort;
+ # for tagging invoice details
+ # (unfortunate; should be a svc_x class method or table_info item or
+ # something)
+ my $phonenum;
+ if ( $svc_table eq 'svc_phone' ) {
+ $phonenum = $svc_x->phonenum;
+ } elsif ( $svc_table eq 'svc_pbx' ) {
+ $phonenum = $svc_x->title;
+ } elsif ( $svc_table eq 'svc_acct' ) {
+ $phonenum = $svc_x->username;
+ }
+ $formatter->phonenum($phonenum);
+
#first rate any outstanding CDRs not yet rated
+ # use FS::Cursor for this starting in 4.x
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
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,
- 'region_group_included_min_hashref' => \%region_group_included_min,
+ 'plan_included_min' => \$included_min_left,
+ 'detail_included_min_hashref' => \%detail_included_min,
);
die $error if $error; #??
$error = $cdr->set_status('done');
}
die $error if $error;
- $formatter->append($cdr);
+ $formatter->append($cdr)
+ unless $cdr->rated_price == 0 and not $usage_showzero;
$cdr_search->adjust(1) if $cdr->freesidestatus eq 'rated';
} #$cdr
$formatter->finish; #writes into $details
unshift @$details, $formatter->header if @$details;
+ if ( $included_min_total > 0 ) {
+
+ my $min_detail = sprintf('%d / %d ',
+ $included_min_total - $included_min_left,
+ $included_min_total
+ ) . $cust_pkg->mt('included minutes used');
+ unshift @$details, $min_detail;
+ }
+
$charges;
}
#returns a reason why not to rate this CDR, or false if the CDR is chargeable
+# lots of false laziness w/voip_inbound
sub check_chargable {
my( $self, $cdr, %flags ) = @_;
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') )"
+
+ my $disposition_prefix = $self->option_cacheable('disposition_prefix');
+ my $len_dis_prefix = length($disposition_prefix);
+ return "disposition does not start with $disposition_prefix"
+ if $len_dis_prefix
+ && substr($cdr->disposition, 0, $len_dis_prefix) ne $disposition_prefix;
+
+ 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') )"
+ # unlike everything else, use_calltypenum is applied in FS::svc_x::get_cdrs.
+ return "calltypenum != ". $self->option_cacheable('use_calltypenum')
+ if length($self->option_cacheable('use_calltypenum'))
+ && $cdr->calltypenum ne $self->option_cacheable('use_calltypenum'); #ne otherwise 0 matches ''
+
+ return "calltypenum == ". $self->option_cacheable('ignore_calltypenum')
+ if length($self->option_cacheable('ignore_calltypenum'))
+ && $cdr->calltypenum eq $self->option_cacheable('ignore_calltypenum'); #eq otherwise 0 matches ''
+
+ 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_dcontext_prefix =
+ length($self->option_cacheable('skip_dcontext_prefix'));
+ return "dcontext starts with ". $self->option_cacheable('skip_dcontext_prefix')
+ if $len_dcontext_prefix
+ && substr($cdr->dcontext,0,$len_dcontext_prefix) eq $self->option_cacheable('skip_dcontext_prefix');
+
+ my $len_suffix = length($self->option_cacheable('skip_dcontext_suffix'));
+ return "dcontext ends with ". $self->option_cacheable('skip_dcontext_suffix')
+ if $len_suffix
+ && substr($cdr->dcontext,-$len_suffix,$len_suffix) eq $self->option_cacheable('skip_dcontext_suffix');
+
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');
and length($cdr->max_callers)
and $cdr->max_callers <= $self->option_cacheable('skip_max_callers');
+ return "calldate < ". $self->option_cacheable('skip_old')
+ if length($self->option_cacheable('skip_old'))
+ && $cdr->calldate_unix < str2time($self->option_cacheable('skip_old'));
+
#all right then, rate it
'';
}
0;
}
-# This equates svc_phone records; perhaps svc_phone should have a field
-# to indicate it represents a line
-sub calc_units {
- my($self, $cust_pkg ) = @_;
- my $count = 0;
- if ( $self->option('count_available_phones', 1)) {
- map { $count += ( $_->quantity || 0 ) }
- grep { $_->part_svc->svcdb eq 'svc_phone' }
- $cust_pkg->part_pkg->pkg_svc;
- } else {
- $count =
- scalar(grep { $_->part_svc->svcdb eq 'svc_phone' } $cust_pkg->cust_svc);
- }
- $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 *
+ ($cust_pkg->quantity || 1),
+ });
+ 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
+# i think this is currently unused?
sub sum_usage {
my $self = shift;
$self->option('output_format') =~ /^sum_/;
1;
-