From de3bf373e97f6875e09fc17d99068a2716bf3b2d Mon Sep 17 00:00:00 2001 From: mark Date: Thu, 1 Jul 2010 01:53:50 +0000 Subject: [PATCH] voip_cdr call rating by day and time, RT#4763 --- FS/FS/Mason.pm | 2 + FS/FS/Schema.pm | 23 ++++ FS/FS/part_pkg/voip_cdr.pm | 160 ++++++++++++++++++------- FS/FS/rate.pm | 34 +++++- FS/FS/rate_detail.pm | 27 +++++ FS/FS/rate_time.pm | 168 ++++++++++++++++++++++++++ FS/FS/rate_time_interval.pm | 162 +++++++++++++++++++++++++ FS/t/rate_time.t | 5 + FS/t/rate_time_interval.t | 5 + httemplate/browse/rate.cgi | 2 + httemplate/browse/rate_detail.html | 205 +++++++++++++++++++++++--------- httemplate/browse/rate_time.html | 48 ++++++++ httemplate/edit/process/rate_time.cgi | 94 +++++++++++++++ httemplate/edit/rate_detail.html | 9 ++ httemplate/edit/rate_time.cgi | 44 +++++++ httemplate/elements/auto-table.html | 146 +++++++++++++++++++++++ httemplate/misc/delete-rate_detail.html | 20 ++++ 17 files changed, 1054 insertions(+), 100 deletions(-) create mode 100644 FS/FS/rate_time.pm create mode 100644 FS/FS/rate_time_interval.pm create mode 100644 FS/t/rate_time.t create mode 100644 FS/t/rate_time_interval.t create mode 100644 httemplate/browse/rate_time.html create mode 100644 httemplate/edit/process/rate_time.cgi create mode 100644 httemplate/edit/rate_time.cgi create mode 100644 httemplate/elements/auto-table.html create mode 100755 httemplate/misc/delete-rate_detail.html diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 6bc233844..f1e5c7de5 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -244,6 +244,8 @@ if ( -e $addl_handler_use_file ) { use FS::cgp_rule_action; use FS::bill_batch; use FS::cust_bill_batch; + use FS::rate_time; + use FS::rate_time_interval; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 73392e86c..f113146ca 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -2229,6 +2229,7 @@ sub tables_hashref { 'conn_sec', 'int', '', '', '0', '', 'min_charge', 'decimal', '', '10,5', '', '', #@money_type, '', '', 'sec_granularity', 'int', '', '', '', '', + 'ratetimenum', 'int', 'NULL', '', '', '', #time period (link to table of periods)? 'classnum', 'int', 'NULL', '', '', '', ], @@ -2260,6 +2261,28 @@ sub tables_hashref { 'index' => [ [ 'countrycode' ], [ 'npa' ], [ 'regionnum' ] ], }, + 'rate_time' => { + 'columns' => [ + 'ratetimenum', 'serial', '', '', '', '', + 'ratetimename', 'varchar', '', $char_d, '', '', + ], + 'primary_key' => 'ratetimenum', + 'unique' => [], + 'index' => [], + }, + + 'rate_time_interval' => { + 'columns' => [ + 'intervalnum', 'serial', '', '', '', '', + 'stime', 'int', '', '', '', '', + 'etime', 'int', '', '', '', '', + 'ratetimenum', 'int', '', '', '', '', + ], + 'primary_key' => 'intervalnum', + 'unique' => [], + 'index' => [], + }, + 'usage_class' => { 'columns' => [ 'classnum', 'serial', '', '', '', '', diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm index 9e21ba243..0ccd83505 100644 --- a/FS/FS/part_pkg/voip_cdr.pm +++ b/FS/FS/part_pkg/voip_cdr.pm @@ -13,6 +13,8 @@ use FS::rate_prefix; use FS::rate_detail; use FS::part_pkg::recur_Common; +use List::Util qw(first min); + @ISA = qw(FS::part_pkg::recur_Common); $DEBUG = 1; @@ -318,6 +320,8 @@ sub calc_usage { @dirass = split(',', $dirass); } + my %interval_cache = (); # for timed rates + #for check_chargable, so we don't keep looking up options inside the loop my %opt_cache = (); @@ -347,11 +351,16 @@ sub calc_usage { my $rate_detail; my( $rate_region, $regionnum ); + my $rate; my $pretty_destnum; my $charge = ''; my $seconds = ''; + my $weektime = ''; my $regionname = ''; my $classnum = ''; + my $countrycode; + my $number; + my @call_details = (); if ( $rating_method eq 'prefix' ) { @@ -378,7 +387,7 @@ sub calc_usage { # (or calling station id for toll free calls) ### - my( $to_or_from, $number ); + my( $to_or_from ); if ( $cdr->is_tollfree && ! $disable_tollfree ) { #tollfree call $to_or_from = 'from'; @@ -398,7 +407,7 @@ sub calc_usage { # $dest =~ s/\@(.*)$// and $siphost = $1; # @10.54.32.1, @sip.example.com #determine the country code - my $countrycode; + $countrycode = ''; if ( $number =~ /^$intl(((\d)(\d))(\d))(\d+)$/ || $number =~ /^\+(((\d)(\d))(\d))(\d+)$/ ) @@ -427,11 +436,20 @@ sub calc_usage { #asterisks here causes inserting the detail to barf, so: $pretty_destnum =~ s/\*//g; - my $rate = qsearchs('rate', { 'ratenum' => $ratenum }) + $rate = qsearchs('rate', { 'ratenum' => $ratenum }) or die "ratenum $ratenum not found!"; + my @ltime = localtime($cdr->startdate); + $weektime = $ltime[0] + + $ltime[1]*60 + #minutes + $ltime[2]*3600 + #hours + $ltime[6]*86400; #days since sunday + # if there's no timed rate_detail for this time/region combination, + # dest_detail returns the default. There may still be a timed rate + # that applies after the starttime of the call, so be careful... $rate_detail = $rate->dest_detail({ 'countrycode' => $countrycode, 'phonenum' => $number, + 'weektime' => $weektime, }); if ( $rate_detail ) { @@ -443,6 +461,17 @@ sub calc_usage { "and rate detail $rate_detail\n" if $DEBUG; + if ( !exists($interval_cache{$regionnum}) ) { + my @intervals = ( + sort { $a->stime <=> $b->stime } + map { my $r = $_->rate_time; $r ? $r->intervals : () } + $rate->rate_detail + ); + $interval_cache{$regionnum} = \@intervals; + warn " cached ".scalar(@intervals)." interval(s)\n" + if $DEBUG; + } + } elsif ( $ignore_unrateable ) { $rate_region = ''; @@ -557,60 +586,111 @@ sub calc_usage { unless ( @call_details || ( $charge ne '' && $charge == 0 ) ) { - $included_min{$regionnum} = $rate_detail->min_included - unless exists $included_min{$regionnum}; + my $seconds_left = $use_duration ? $cdr->duration : $cdr->billsec; + # charge for the first (conn_sec) seconds + $seconds = min($seconds_left, $rate_detail->conn_sec); + $seconds_left -= $seconds; + $weektime += $seconds; + $charge = sprintf("%.02f", $rate_detail->conn_charge); + + my $total_minutes = 0; + my $etime; + while($seconds_left) { + my $ratetimenum = $rate_detail->ratetimenum; # may be empty + + # find the end of the current rate interval + if(@{ $interval_cache{$regionnum} } == 0) { + # There are no timed rates in this group, so just stay + # in the default rate_detail for the entire duration. + $etime = 0; + } + elsif($ratetimenum) { + # This is a timed rate, so go to the etime of this interval. + # If it's followed by another timed rate, the stime of that + # interval should match the etime of this one. + my $interval = $rate_detail->rate_time->contains($weektime); + $etime = $interval->etime; + } + else { + # This is a default rate, so use the stime of the next + # interval in the sequence. + my $next_int = first { $_->stime > $weektime } + @{ $interval_cache{$regionnum} }; + if ($next_int) { + $etime = $next_int->stime; + } + else { + # weektime is near the end of the week, so decrement + # it by a full week and use the stime of the first + # interval. + $weektime -= (3600*24*7); + $etime = $interval_cache{$regionnum}->[0]->stime; + } + } - my $granularity = $rate_detail->sec_granularity; + my $charge_sec = min($seconds_left, $etime - $weektime); - # length($cdr->billsec) ? $cdr->billsec : $cdr->duration; - $seconds = $use_duration ? $cdr->duration : $cdr->billsec; + $seconds_left -= $charge_sec; + $seconds += $charge_sec; - $seconds -= $rate_detail->conn_sec; - $seconds = 0 if $seconds < 0; + $included_min{$regionnum}{$ratetimenum} = $rate_detail->min_included + unless exists $included_min{$regionnum}{$ratetimenum}; - $seconds += $granularity - ( $seconds % $granularity ) - if $seconds # don't granular-ize 0 billsec calls (bills them) - && $granularity; # 0 is per call - my $minutes = sprintf("%.1f", $seconds / 60); - $minutes =~ s/\.0$// if $granularity == 60; + my $granularity = $rate_detail->sec_granularity; - # per call rather than per minute - $minutes = 1 unless $granularity; + # should this be done in every rate interval? + $charge_sec += $granularity - ( $charge_sec % $granularity ) + if $charge_sec # don't granular-ize 0 billsec calls (bills them) + && $granularity; # 0 is per call + my $minutes = sprintf("%.1f", $charge_sec / 60); + $minutes =~ s/\.0$// if $granularity == 60; - $included_min{$regionnum} -= $minutes; + # per call rather than per minute + $minutes = 1 unless $granularity; + $seconds_left = 0 unless $granularity; - $charge = sprintf('%.2f', $rate_detail->conn_charge); + $included_min{$regionnum}{$ratetimenum} -= $minutes; + + if ( $included_min{$regionnum}{$ratetimenum} <= 0 ) { + my $charge_min = 0 - $included_min{$regionnum}{$ratetimenum}; #XXX should preserve + #(display?) this + $included_min{$regionnum}{$ratetimenum} = 0; + $charge += sprintf('%.2f', ($rate_detail->min_charge * $charge_min) + + 0.00000001 ); #so 1.005 rounds to 1.01 + $total_minutes += $minutes; + } - if ( $included_min{$regionnum} < 0 ) { - my $charge_min = 0 - $included_min{$regionnum}; #XXX should preserve - #(display?) this - $included_min{$regionnum} = 0; - $charge += sprintf('%.2f', ($rate_detail->min_charge * $charge_min) - + 0.00000001 ); #so 1.005 rounds to 1.01 - $charge = sprintf('%.2f', $charge); - } - warn "Incrementing \$charges by $charge. Now $charges\n" if $DEBUG; - $charges += $charge; + # choose next rate_detail + $rate_detail = $rate->dest_detail({ 'countrycode' => $countrycode, + 'phonenum' => $number, + 'weektime' => $etime }) + if($seconds_left); + # we have now moved forward to $etime + $weektime = $etime; + } #while $seconds_left # this is why we need regionnum/rate_region.... warn " (rate region $rate_region)\n" if $DEBUG; - @call_details = ( - $cdr->downstream_csv( 'format' => $output_format, - 'granularity' => $granularity, - 'minutes' => $minutes, - 'charge' => $charge, - 'pretty_dst' => $pretty_destnum, - 'dst_regionname' => $regionname, - ) - ); - $classnum = $rate_detail->classnum; + $charge = sprintf('%.2f', $charge); - } + @call_details = ( + $cdr->downstream_csv( 'format' => $output_format, + 'granularity' => $rate_detail->sec_granularity, + 'minutes' => $total_minutes, + 'charge' => $charge, + 'pretty_dst' => $pretty_destnum, + 'dst_regionname' => $regionname, + ) + ); + } #if(there is a rate_detail) + if ( $charge > 0 ) { #just use FS::cust_bill_pkg_detail objects? + warn "Incrementing \$charges by $charge. Now $charges\n" if $DEBUG; + $charges += $charge; my $call_details; my $phonenum = $cust_svc->svc_x->phonenum; diff --git a/FS/FS/rate.pm b/FS/FS/rate.pm index 793846a7f..f30e4c772 100644 --- a/FS/FS/rate.pm +++ b/FS/FS/rate.pm @@ -279,16 +279,22 @@ Destination can be specified as an FS::rate_detail object or regionnum (see L), or as a hashref with two keys: I and I. +An optional third key, I, will return a timed rate (one with +a non-null I) if one exists for a call at that time. If +no matching timed rate exists, the non-timed rate will be returned. + =cut sub dest_detail { my $self = shift; my $regionnum; + my $weektime; if ( ref($_[0]) eq 'HASH' ) { my $countrycode = $_[0]->{'countrycode'}; my $phonenum = $_[0]->{'phonenum'}; + $weektime = $_[0]->{'weektime'}; #find a rate prefix, first look at most specific, then fewer digits, # finally trying the country code only @@ -314,9 +320,31 @@ sub dest_detail { } else { $regionnum = ref($_[0]) ? shift->regionnum : shift; } - - qsearchs( 'rate_detail', { 'ratenum' => $self->ratenum, - 'dest_regionnum' => $regionnum, } ); + + if(!defined($weektime)) { + return qsearchs( 'rate_detail', + { 'ratenum' => $self->ratenum, + 'dest_regionnum' => $regionnum, + 'ratetimenum' => '', + } ); + } + else { + my @details = grep { my $rate_time = $_->rate_time; + $rate_time && $rate_time->contains($weektime) } + qsearch( 'rate_detail', + { 'ratenum' => $self->ratenum, + 'dest_regionnum' => $regionnum, } ); + if(!@details) { + # this may change at some point + return $self->dest_detail($regionnum); + } + elsif(@details == 1) { + return $details[0]; + } + else { + die "overlapping rate_detail times (region $regionnum, time $weektime)\n"; + } + } } =item rate_detail diff --git a/FS/FS/rate_detail.pm b/FS/FS/rate_detail.pm index f6cdedf6e..7b9045205 100644 --- a/FS/FS/rate_detail.pm +++ b/FS/FS/rate_detail.pm @@ -5,6 +5,7 @@ use vars qw( @ISA $DEBUG $me ); use FS::Record qw( qsearch qsearchs dbh ); use FS::rate; use FS::rate_region; +use FS::rate_time; use Tie::IxHash; @ISA = qw(FS::Record); @@ -54,6 +55,8 @@ inherits from FS::Record. The following fields are currently supported: =item classnum - usage class (see L) if any for this rate +=item ratetimenum - rating time period (see Ldest_region->prefixes_short; } +=item rate_time + +Returns the L object associated with this call +plan rate, if there is one. + +=cut + +sub rate_time { + my $self = shift; + $self->ratetimenum ? FS::rate_time->by_key($self->ratetimenum) : (); +} + +=item rate_time_name + +Returns the I field of the L object +associated with this rate plan. + +=cut + +sub rate_time_name { + my $self = shift; + $self->ratetimenum ? $self->rate_time->ratetimename : '(default)'; +} + =item classname Returns the name of the usage class (see L) associated with diff --git a/FS/FS/rate_time.pm b/FS/FS/rate_time.pm new file mode 100644 index 000000000..40cd23e9c --- /dev/null +++ b/FS/FS/rate_time.pm @@ -0,0 +1,168 @@ +package FS::rate_time; + +use strict; +use base qw( FS::Record ); +use FS::Record qw( qsearch qsearchs ); +use FS::rate_time_interval; + +=head1 NAME + +FS::rate_time - Object methods for rate_time records + +=head1 SYNOPSIS + + use FS::rate_time; + + $record = new FS::rate_time \%hash; + $record = new FS::rate_time { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::rate_time object represents a time period for selection of CDR billing +rates. FS::rate_time inherits from FS::Record. The following fields are +currently supported: + +=over 4 + +=item ratetimenum + +primary key + +=item ratetimename + +A label (like "Daytime" or "Weekend"). + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new example. To add the example to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I method. + +=cut + +# the new method can be inherited from FS::Record, if a table method is defined + +sub table { 'rate_time'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +# the insert method can be inherited from FS::Record + +=item delete + +Delete this record from the database. + +=cut + +# the delete method can be inherited from FS::Record + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +# the replace method can be inherited from FS::Record + +=item check + +Checks all fields to make sure this is a valid example. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('ratetimenum') + || $self->ut_text('ratetimename') + ; + return $error if $error; + + $self->SUPER::check; +} + +=item intervals + +Return the L objects included in this rating period. + +=cut + +sub intervals { + my $self = shift; + return qsearch({ table => 'rate_time_interval', + hashref => { ratetimenum => $self->ratetimenum }, + order_by => 'ORDER BY stime ASC', + }); +} + +=item contains TIME + +Return the L object that contains the specified +time-of-week (in seconds from the start of Sunday). The primary use of +this is to test whether that time falls within this rating period. + +=cut + +sub contains { + my $self = shift; + my $weektime = shift; + return qsearchs('rate_time_interval', { ratetimenum => $self->ratetimenum, + stime => { op => '<=', + value => $weektime }, + etime => { op => '>', + value => $weektime }, + } ); +} + +=item description + +Returns a list of arrayrefs containing the starting and +ending times of each interval in this period, in a readable +format. + +=cut + +sub description { + my $self = shift; + return map { [ $_->description ] } $self->intervals; +} + + +=back + +=head1 BUGS + +To be seen. + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/rate_time_interval.pm b/FS/FS/rate_time_interval.pm new file mode 100644 index 000000000..37bc83bdf --- /dev/null +++ b/FS/FS/rate_time_interval.pm @@ -0,0 +1,162 @@ +package FS::rate_time_interval; + +use strict; +use base qw( FS::Record ); +use FS::Record qw( qsearch qsearchs ); + +=head1 NAME + +FS::rate_time_interval - Object methods for rate_time_interval records + +=head1 SYNOPSIS + + use FS::rate_time_interval; + + $record = new FS::rate_time_interval \%hash; + $record = new FS::rate_time_interval { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::rate_time_interval object represents an interval of clock time during +the week, such as "Monday, 7 AM to 8 PM". FS::rate_time_interval inherits +from FS::Record. The following fields are currently supported: + +=over 4 + +=item intervalnum + +primary key + +=item stime + +Start of the interval, in seconds from midnight on Sunday. + +=item etime + +End of the interval. + +=item ratetimenum + +A foreign key to an L object representing the set of intervals +to which this belongs. + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new example. To add the example to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I method. + +=cut + +# the new method can be inherited from FS::Record, if a table method is defined + +sub table { 'rate_time_interval'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +# the insert method can be inherited from FS::Record + +=item delete + +Delete this record from the database. + +=cut + +# the delete method can be inherited from FS::Record + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +# the replace method can be inherited from FS::Record + +=item check + +Checks all fields to make sure this is a valid example. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('intervalnum') + || $self->ut_number('stime') + || $self->ut_number('etime') + || $self->ut_number('ratetimenum') + ; + return $error if $error; + + $self->SUPER::check; +} + +=item rate_time + +Returns the L comprising this interval. + +=cut + +sub rate_time { + my $self = shift; + FS::rate_time->by_key($self->ratetimenum); +} + +=item description + +Returns two strings containing stime and etime, formatted +"Day HH:MM:SS AM/PM". Example: "Mon 5:00 AM". + +=cut + +my @days = qw(Sun Mon Tue Wed Thu Fri Sat); + +sub description { + my $self = shift; + return map { + sprintf('%s %02d:%02d:%02d %s', + $days[int($_/86400) % 7], + int($_/3600) % 12, + int($_/60) % 60, + $_ % 60, + (($_/3600) % 24 < 12) ? 'AM' : 'PM' ) + } ( $self->stime, $self->etime ); +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L, L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/t/rate_time.t b/FS/t/rate_time.t new file mode 100644 index 000000000..ece37d198 --- /dev/null +++ b/FS/t/rate_time.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::rate_time; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/rate_time_interval.t b/FS/t/rate_time_interval.t new file mode 100644 index 000000000..68f9a9590 --- /dev/null +++ b/FS/t/rate_time_interval.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::rate_time_interval; +$loaded=1; +print "ok 1\n"; diff --git a/httemplate/browse/rate.cgi b/httemplate/browse/rate.cgi index 02d670fbd..428158a6d 100644 --- a/httemplate/browse/rate.cgi +++ b/httemplate/browse/rate.cgi @@ -2,6 +2,8 @@ 'title' => 'Rate plans', 'menubar' => [ 'Regions and Prefixes' => $p.'browse/rate_region.html', + 'Time Periods' => + $p.'browse/rate_time.html', ], 'html_init' => $html_init, 'name' => 'rate plans', diff --git a/httemplate/browse/rate_detail.html b/httemplate/browse/rate_detail.html index 3371926b4..faaec2c38 100644 --- a/httemplate/browse/rate_detail.html +++ b/httemplate/browse/rate_detail.html @@ -1,39 +1,53 @@ -<% include( 'elements/browse.html', - 'title' => $title, - 'name_singular' => 'rate', - 'html_init' => $html_init, - 'menubar' => [ 'Rate plans' => $p.'browse/rate.cgi' ], - 'query' => { - 'table' => 'rate_detail', - 'addl_from' => $join, - 'hashref' => { 'ratenum' => $ratenum }, - 'extra_sql' => $where, - }, - 'count_query' => "SELECT COUNT(*) FROM rate_detail $join". - " WHERE ratenum = $ratenum $where", - 'header' => [ - 'Region', - 'Prefix(es)', - 'Included
minutes', - 'Connection
charge', - 'Charge per
minute', - 'Granularity', - 'Usage class', - ], - 'fields' => [ - 'regionname', - sub { shift->dest_region->prefixes_short }, - sub { shift->min_included. $edit_hint }, - $conn_charge_sub, - sub { $money_char. shift->min_charge. $edit_hint }, - sub { $granularity{ shift->sec_granularity } }, - 'classname', - ], - 'links' => [ '', '', $edit_link, $edit_link, '', '' ], - 'link_onclicks' => [ '', '', $edit_onclick, $edit_onclick, '', '' ], - 'align' => 'llrrcc', - ) -%> +<% include('/elements/init_overlib.html') %> +<% include('/elements/header.html',$title) %> +<% include('/elements/menubar.html',@menubar) %> +

+<% include('/elements/table-grid.html') %> + +% my $col = 0; +% foreach (@header) { +% my $hlink = $hlinks[$col]; + + <% $hlink ? qq!$_! : $_ %> + +% $col++; +% } #foreach @header + +% my $row = 0; +% foreach my $region (@rate_region) { +% $col = 0; +% foreach ($region->regionname, $region->prefixes_short) { + + <% $_ %> + +% } +% foreach my $rate_time (@rate_time, '') { + +% my $detail = $details[$row][$col]; +% if($detail) { + + + <% granularity_detail($detail) %> + <% min_included_detail($detail) %> + <% conn_charge_detail($detail) %> + +
<% edit_link($detail) %><% $money_char.$detail->min_charge %> + <% $detail->sec_granularity ? ' / minute':' / call' %> + <% $edit_hint %> +
<% $rate_time ? delete_link($detail) : '' %>
+% } +% else { #!$detail + <% add_link($ratenum, $region, $rate_time) %> +% } +% $col++; + +% } # foreach @rate_time + +% $row++; +% }# foreach @rate_region +<% include('/elements/footer.html') %> + <%once> tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities(); @@ -42,31 +56,92 @@ tie my %conn_secs, 'Tie::IxHash', FS::rate_detail::conn_secs(); my $conf = new FS::Conf; my $money_char = $conf->config('money_char') || '$'; -my $join = - ' JOIN rate_region ON ( rate_detail.dest_regionnum = rate_region.regionnum )'; +my @menubar = ( 'Rate plans' => $p.'browse/rate.cgi', + 'Regions and Prefixes' => $p.'browse/rate_region.html', + 'Time Periods' => $p.'browse/rate_time.html', + ); -my $edit_link = [ 'javascript:void(0);', sub { ''; } ]; +sub small { + ''.shift.'' +} +my $edit_hint = small('(edit)'); -my $edit_onclick = sub { +sub edit_link { my $rate_detail = shift; my $ratedetailnum = $rate_detail->ratedetailnum; + ' 'Edit rate', 'height' => 420, #default# 'width' => 540, #default# 'color' => '#333399', - ); -}; -my $edit_hint = ' (edit)'; - -my $conn_charge_sub = sub { - my $rate_detail = shift; - #return '' unless $rate_detail->conn_charge > 0 || $rate_detail->conn_sec; - $money_char. $rate_detail->conn_charge. - ($rate_detail->conn_sec ? ' for '.$conn_secs{$rate_detail->conn_sec} : ''). - $edit_hint; -}; + ) . '">' +} + +sub add_link { + my ($ratenum, $region, $rate_time) = @_; + ' 'Add rate', + 'height' => 420, + ).'">'.small('(add)').'' +} + +sub delete_link { + my $rate_detail = shift; + my $ratedetailnum = $rate_detail->ratedetailnum; + my $onclick = include( '/elements/popup_link_onclick.html', + 'action' => "${p}misc/delete-rate_detail.html?$ratedetailnum", + 'actionlabel' => 'Delete rate', + 'width' => 510, + 'height' => 315, + 'frame' => 'top', + ); + $onclick = "if(confirm('Delete this rate?')) { $onclick }"; + qq!!.small('(delete)').'' +} + +sub granularity_detail { + my $rate_detail = shift; + if($rate_detail->sec_granularity != 60 && $rate_detail->sec_granularity > 0) { + ''. + small('in '.$granularity{$rate_detail->sec_granularity}.' increments'). + ''; + } + else { '' } +} + +sub min_included_detail { + my $rate_detail = shift; + if($rate_detail->min_included) { + ''. + small( $rate_detail->min_included . + ($rate_detail->sec_granularity ? + ' minutes included' : + ' calls included') ). + '' + } + else { '' } +} + +sub conn_charge_detail { + my $rate_detail = shift; + if($rate_detail->conn_charge > 0) { + #return '' unless $rate_detail->conn_charge > 0 || $rate_detail->conn_sec; + ''. + small( $money_char. $rate_detail->conn_charge. + ' for '.$conn_secs{$rate_detail->conn_sec} + ). + '' + } + else { '' } +} <%init> @@ -74,8 +149,6 @@ my $conn_charge_sub = sub { die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); -my $html_init = include('/elements/init_overlib.html'); - $cgi->param('ratenum') =~ /^(\d+)$/ or die "unparsable ratenum"; my $ratenum = $1; my $rate = qsearchs('rate', { 'ratenum' => $ratenum } ) @@ -83,11 +156,10 @@ my $rate = qsearchs('rate', { 'ratenum' => $ratenum } ) my $ratename = $rate->ratename; my $title = "$ratename rates"; -my @where = (); - +my $where; if ( $cgi->param('countrycode') =~ /^(\d+)$/ ) { my $countrycode = $1; - push @where, "0 < ( SELECT COUNT(*) FROM rate_prefix + $where = "WHERE 0 < ( SELECT COUNT(*) FROM rate_prefix WHERE rate_prefix.regionnum = rate_region.regionnum AND countrycode = '$countrycode' ) @@ -95,6 +167,25 @@ if ( $cgi->param('countrycode') =~ /^(\d+)$/ ) { $title .= " for +$countrycode"; } -my $where = scalar(@where) ? ' AND '.join(' AND ', @where ) : ''; +my @rate_region = qsearch({ table => 'rate_region', + hashref => {}, + extra_sql => $where, + }); + +my @rate_time = qsearch('rate_time', {}); +my @header = ('Region', 'Prefix(es)', + map( { $_->ratetimename } @rate_time ), + '(default)'); +my @hlinks = map {''} @header; + +my @rtns = ( map( { $_->ratetimenum } @rate_time ), '' ); +my @details; +foreach my $region (@rate_region) { + push @details, [ map { qsearchs('rate_detail', + { 'ratenum' => $ratenum, + 'dest_regionnum' => $region->regionnum, + 'ratetimenum' => $_ } ) or '' + } @rtns ]; +} diff --git a/httemplate/browse/rate_time.html b/httemplate/browse/rate_time.html new file mode 100644 index 000000000..416ded41f --- /dev/null +++ b/httemplate/browse/rate_time.html @@ -0,0 +1,48 @@ +<% include( 'elements/browse.html', + 'title' => 'Rating Time Periods', + 'name_singular' => 'period', + 'menubar' => [ 'Rate plans' => $p.'browse/rate.cgi' ], + 'html_init' => $html_init, + 'query' => { + 'table' => 'rate_time', + 'order_by' => 'ratetimenum', # lacking anything else + 'hashref' => {}, + }, + 'count_query' => 'SELECT COUNT(*) FROM rate_time', + 'header' => \@header, + 'fields' => \@fields, + 'links' => \@links, + 'align' => \@align, + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $edit_url = $p.'edit/rate_time.cgi'; + +my $link = [ "$edit_url?", 'ratetimenum' ]; + +my $html_init = + 'Time periods for VoIP and call billing.

'. + qq(Add a new period

); + +sub interval { + my $i = shift; + '' + .join('', map { '' } + shift->intervals + ) . '
'.($_->description)[$i].'
'; +} + +# inefficient but readable +my $stime_sub = sub { interval(0,shift) }; +my $etime_sub = sub { interval(1,shift) }; + +my @header = ( '#', 'Period', 'Start', 'End' ); +my @fields = ( 'ratetimenum', 'ratetimename', $stime_sub, $etime_sub ); +my @links = ( ($link) x 2 ); +my @align = ( 'right', 'left', 'left' ); + + diff --git a/httemplate/edit/process/rate_time.cgi b/httemplate/edit/process/rate_time.cgi new file mode 100644 index 000000000..48ed2739e --- /dev/null +++ b/httemplate/edit/process/rate_time.cgi @@ -0,0 +1,94 @@ +% if ( $error ) { +% $cgi->param('error', $error); +<% $cgi->redirect(popurl(2). "rate_time.cgi?". $cgi->query_string ) %> +% } else { +<% $cgi->redirect(popurl(3). "browse/rate_time.html" ) %> +% } +%# dumper_html(\%vars, \%old_ints, {$rate_time->intervals}) %> +<%init> +my $error = ''; +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); +my $ratetimenum = $cgi->param('ratetimenum'); +my $ratetimename = $cgi->param('ratetimename'); +my $delete = $cgi->param('delete'); + +my %vars = $cgi->Vars; +#warn Dumper(\%vars)."\n"; + +my $rate_time; + +my %old_ints; +if( $ratetimenum ) { + # editing + $rate_time = FS::rate_time->by_key($ratetimenum); + + # make a list of existing intervals that will be deleted + foreach ($rate_time->intervals) { + $old_ints{$_->intervalnum} = $_; + } + + if ( $delete ) { + $error = $rate_time->delete; + # intervals will be deleted later + } + elsif( $ratetimename ne $rate_time->ratetimename ) { + # the only case where the rate_time itself must be replaced + $rate_time->ratetimename($ratetimename); + $error = $rate_time->replace; + } +} +else { #!$ratetimenum, adding new + $rate_time = FS::rate_time->new({ ratetimename => $ratetimename }); + $error = $rate_time->insert; + $ratetimenum = $rate_time->ratetimenum; +} + +if(!$delete and !$error) { + foreach my $i (map { /stime(\d+)/ } keys(%vars)) { + my $stime = str2wtime($vars{"stime$i"}); + my $etime = str2wtime($vars{"etime$i"}); + next if !defined($stime) or !defined($etime); + #warn "$i: $stime-$etime"; + # try to avoid needlessly wiping and replacing intervals every + # time this is edited. + if( %old_ints ) { + my $this_int = qsearchs('rate_time_interval', + { ratetimenum => $ratetimenum, + stime => $stime, + etime => $etime, } ); + if($this_int) { + delete $old_ints{$this_int->intervalnum}; + #warn "not deleting $stime-$etime\n"; + next; #$i + } + } + my $new_int = FS::rate_time_interval->new({ ratetimenum => $ratetimenum, + stime => $stime, + etime => $etime, } ); + $error = $new_int->insert; + #warn "inserting $stime-$etime\n"; + last if $error; + } +} +if(!$error) { + foreach (values(%old_ints)) { + $error = $_->delete; + #warn "deleting ".$_->stime.' '.$_->etime."\n"; + last if $error; + } +} + +sub str2wtime { + my %days; + @days{qw(Sun Mon Tue Wed Thu Fri Sat)} = (0..6); + my $str = shift; + my ($d, $h, $m, $s, $ampm) = + ($str =~ /^(\w{3}) (\d{2}):(\d{2}):(\d{2}) (\w{2})$/); + return () if !$d; + $h += 24*$days{$d} + ($ampm eq 'PM' ? 12 : 0); + $m += 60*$h; + $s += 60*$m; + return $s; +} + diff --git a/httemplate/edit/rate_detail.html b/httemplate/edit/rate_detail.html index 869ace8d4..ec04e4cbb 100644 --- a/httemplate/edit/rate_detail.html +++ b/httemplate/edit/rate_detail.html @@ -5,6 +5,7 @@ 'labels' => { 'ratedetailnum' => 'Rate', #should hide... 'dest_regionname' => 'Region', 'dest_prefixes_short' => 'Prefix(es)', + 'rate_time_name' => 'Time period', 'min_included' => 'Included minutes/calls', 'conn_charge' => 'Connection charge', 'conn_sec' => 'For', @@ -16,8 +17,10 @@ { field=>'ratenum', type=>'hidden', }, { field=>'orig_regionnum', type=>'hidden', }, { field=>'dest_regionnum', type=>'hidden', }, + { field=>'ratetimenum', type=>'hidden', }, { field=>'dest_regionname', type=>'fixed', }, { field=>'dest_prefixes_short', type=>'fixed', }, + { field=>'rate_time_name', type=>'fixed', }, { field=>'min_included', type=>'text', size=>5 }, { field=>'conn_charge', type=>'money', size=>4 }, { field =>'conn_sec', @@ -42,6 +45,12 @@ }, ], + 'new_hashref_callback' => sub { + { ratenum => $cgi->param('ratenum'), + dest_regionnum => $cgi->param('dest_regionnum'), + ratetimenum => $cgi->param('ratetimenum'), + } + }, ) %> <%once> diff --git a/httemplate/edit/rate_time.cgi b/httemplate/edit/rate_time.cgi new file mode 100644 index 000000000..230aef82d --- /dev/null +++ b/httemplate/edit/rate_time.cgi @@ -0,0 +1,44 @@ +<% include("/elements/header.html","$action Time Period", menubar( + 'Rate plans' => "${p}browse/rate.cgi", + ) ) +%> + +<% include('/elements/error.html') %> + +
"> + +<% ntable('#cccccc',2) %> + + + + + +
Period name
+<% include('/elements/auto-table.html', + header => [ 'Start', 'End' ], + fields => [ 'stime', 'etime' ], + size => [ 18, 18 ], + maxl => [ 15, 15 ], + align => [ 'right', 'right' ], + data => \@data, + ) %> + +
+
+">Delete this period +<% include('/elements/footer.html') %> + +<%init> +my $ratetimenum = ($cgi->keywords)[0] || ''; +my $action = 'Add'; +my $rate_time; +my @data = (); + +if($ratetimenum) { + $action = 'Edit'; + $rate_time = qsearchs('rate_time', {ratetimenum => $ratetimenum}) + or die "ratetimenum $ratetimenum not found"; + @data = $rate_time->description; +} + + diff --git a/httemplate/elements/auto-table.html b/httemplate/elements/auto-table.html new file mode 100644 index 000000000..89d6eacb9 --- /dev/null +++ b/httemplate/elements/auto-table.html @@ -0,0 +1,146 @@ +<%doc> + +Example: +<% include('/elements/auto-table.html', + + ### + # required + ### + + 'header' => [ '#', 'Item', 'Amount' ], + 'fields' => [ 'id', 'name', 'amount' ], + + ### + # highly recommended + ### + + 'size' => [ 4, 12, 8 ], + 'maxl' => [ 4, 12, 8 ], + 'align' => [ 'right', 'left', 'right' ], + + ### + # optional + ### + + 'data' => [ [ 1, 'Widget', 25 ], + [ 12, 'Super Widget, 7 ] ], + #or + 'records' => [ qsearch('item', { } ) ], + # or any other array of FS::Record objects + + 'prefix' => 'mytable_', +) %> + +Values will be passed through as "mytable_id1", etc. + + + + +% foreach (@header) { + +% } + +% my $row = 0; +% for ( $row = 0; $row < scalar @data; $row++ ) { + +% my $col = 0; +% for ( $col = 0; $col < scalar @fields; $col++ ) { +% my $id = $prefix . $fields[$col] . $row; + +% } + + +% } +
<% $_ %>
+ + MAXLENGTH = <% $maxl[$col] %> + STYLE = "text-align:<% $align[$col] %>" + VALUE = "<% $data[$row][$col] %>" + onchange = "possiblyAddRow();" + > + + " + ALT = "X" + onclick = "deleteThisRow(this);" + > +
+ + + +<%init> +my %opt = @_; + +my @header = @{ $opt{'header'} }; +my @fields = @{ $opt{'fields'} }; +my @data = (); +if($opt{'data'}) { + @data = @{ $opt{'data'} }; +} +elsif($opt{'records'}) { + foreach my $rec (@{ $opt{'records'} }) { + push @data, [ map { $rec->getfield($_) } @fields ]; + } +} +# else @data = (); + +my $prefix = $opt{'prefix'}; +my @size = $opt{'size'} ? @{ $opt{'size'} } : (map {16} @fields); +my @maxl = $opt{'maxl'} ? @{ $opt{'maxl'} } : @size; +my @align = $opt{'align'} ? @{ $opt{'align'} } : (map {'right'} @fields); + + diff --git a/httemplate/misc/delete-rate_detail.html b/httemplate/misc/delete-rate_detail.html new file mode 100755 index 000000000..30856a73a --- /dev/null +++ b/httemplate/misc/delete-rate_detail.html @@ -0,0 +1,20 @@ +% if ( $error ) { +% errorpage($error); +% } else { +<% header('Rate deleted') %> + + +% } +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my ($query) = $cgi->keywords; +$query =~ /^(\d+)$/ or die "Illegal ratedetailnum"; +my $rate_detail = FS::rate_detail->by_key($1); +my $error = $rate_detail->delete; + + -- 2.11.0