diff options
| author | mark <mark> | 2010-07-01 01:53:50 +0000 | 
|---|---|---|
| committer | mark <mark> | 2010-07-01 01:53:50 +0000 | 
| commit | de3bf373e97f6875e09fc17d99068a2716bf3b2d (patch) | |
| tree | bf5d4908cb57519a99cb8db0266f40d671f9e170 | |
| parent | 6643a544400f5bed64b75720fdaccb41f01d1e79 (diff) | |
voip_cdr call rating by day and time, RT#4763
| -rw-r--r-- | FS/FS/Mason.pm | 2 | ||||
| -rw-r--r-- | FS/FS/Schema.pm | 23 | ||||
| -rw-r--r-- | FS/FS/part_pkg/voip_cdr.pm | 160 | ||||
| -rw-r--r-- | FS/FS/rate.pm | 34 | ||||
| -rw-r--r-- | FS/FS/rate_detail.pm | 27 | ||||
| -rw-r--r-- | FS/FS/rate_time.pm | 168 | ||||
| -rw-r--r-- | FS/FS/rate_time_interval.pm | 162 | ||||
| -rw-r--r-- | FS/t/rate_time.t | 5 | ||||
| -rw-r--r-- | FS/t/rate_time_interval.t | 5 | ||||
| -rw-r--r-- | httemplate/browse/rate.cgi | 2 | ||||
| -rw-r--r-- | httemplate/browse/rate_detail.html | 205 | ||||
| -rw-r--r-- | httemplate/browse/rate_time.html | 48 | ||||
| -rw-r--r-- | httemplate/edit/process/rate_time.cgi | 94 | ||||
| -rw-r--r-- | httemplate/edit/rate_detail.html | 9 | ||||
| -rw-r--r-- | httemplate/edit/rate_time.cgi | 44 | ||||
| -rw-r--r-- | httemplate/elements/auto-table.html | 146 | ||||
| -rwxr-xr-x | httemplate/misc/delete-rate_detail.html | 20 | 
17 files changed, 1054 insertions, 100 deletions
| 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<FS::rate_detail>), or as a hashref with two keys: I<countrycode>  and I<phonenum>. +An optional third key, I<weektime>, will return a timed rate (one with  +a non-null I<ratetimenum>) 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<FS::usage_class>) if any for this rate +=item ratetimenum - rating time period (see L<FS::rate_time) if any +  =back  =head1 METHODS @@ -194,6 +197,30 @@ sub dest_prefixes_short {    $self->dest_region->prefixes_short;  } +=item rate_time + +Returns the L<FS::rate_time> 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<ratetimename> field of the L<FS::rate_time> 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<FS::usage_class>) 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<hash> 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<FS::rate_time_interval> 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<FS::rate_time_interval> 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<FS::Record>, 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<FS::rate_time> 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<hash> 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<FS::rate_time> 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<FS::rate_time>, L<FS::Record>, 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<BR>minutes', -                           'Connection<BR>charge', -                           'Charge per<BR>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) %> +<BR><BR> +<% include('/elements/table-grid.html') %> +<TR> +% my $col = 0; +% foreach (@header) { +%   my $hlink = $hlinks[$col]; +  <TH CLASS   = "grid", +      BGCOLOR = "#cccccc"> +  <% $hlink ? qq!<A HREF="$hlink">$_</A>! : $_ %> +  </TH> +%   $col++; +% } #foreach @header +</TR><TR> +% my $row = 0; +% foreach my $region (@rate_region) { +%   $col = 0; +%   foreach ($region->regionname, $region->prefixes_short) { +  <TD> +    <A HREF="<% $p.'edit/rate_region.cgi?'.$region->regionnum %>"><% $_ %></A> +  </TD> +%   } +%   foreach my $rate_time (@rate_time, '') { +  <TD> +%     my $detail = $details[$row][$col]; +%     if($detail) { +      <TABLE CLASS="inv" STYLE="border:none"> +      <TR><TD><% edit_link($detail) %><% $money_char.$detail->min_charge %> +              <% $detail->sec_granularity ? ' / minute':' / call' %> +      <% $edit_hint %></A> +      </TD></TR> +      <% granularity_detail($detail) %> +      <% min_included_detail($detail) %> +      <% conn_charge_detail($detail) %> +      <TR><TD><% $rate_time ? delete_link($detail) : '' %></TD></TR> +    </TABLE> +%     } +%     else { #!$detail +    <% add_link($ratenum, $region, $rate_time) %> +%     } +%     $col++; +  </TD> +%   } # foreach @rate_time +</TR> +%   $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 { +  '<FONT SIZE="-1">'.shift.'</FONT>' +} +my $edit_hint = small('(edit)'); -my $edit_onclick = sub { +sub edit_link {    my $rate_detail = shift;    my $ratedetailnum = $rate_detail->ratedetailnum; +  '<A HREF="javascript:void(0);" onclick="'.    include( '/elements/popup_link_onclick.html',               'action'      => "${p}edit/rate_detail.html?$ratedetailnum",               'actionlabel' => 'Edit rate',               'height'      => 420,               #default# 'width'       => 540,               #default# 'color'       => '#333399', -         ); -}; -my $edit_hint = ' <FONT SIZE="-1">(edit)</FONT>'; - -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) = @_; +  '<A HREF="javascript:void(0);" onclick="'. +  include( '/elements/popup_link_onclick.html', +             'action'      => "${p}edit/rate_detail.html?ratenum=$ratenum". +                               ';dest_regionnum='.$region->regionnum. +                               ($rate_time ?  +                                 ';ratetimenum='.$rate_time->ratetimenum : +                                 ''), +             'actionlabel' => 'Add rate', +             'height'      => 420, +             ).'">'.small('(add)').'</A>' +} + +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!<A HREF="javascript:void(0);" onclick="$onclick">!.small('(delete)').'</A>' +} + +sub granularity_detail { +  my $rate_detail = shift; +  if($rate_detail->sec_granularity != 60 && $rate_detail->sec_granularity > 0) { +    '<TR><TD>'. +    small('in '.$granularity{$rate_detail->sec_granularity}.' increments'). +    '</TD></TR>'; +  } +  else { '' } +} + +sub min_included_detail { +  my $rate_detail = shift; +  if($rate_detail->min_included) { +    '<TR><TD>'. +    small( $rate_detail->min_included .  +            ($rate_detail->sec_granularity ?  +             ' minutes included' :  +             ' calls included') ). +    '</TD></TR>' +  } +  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; +    '<TR><TD>'. +    small( $money_char. $rate_detail->conn_charge. +      ' for '.$conn_secs{$rate_detail->conn_sec} +    ). +    '</TD></TR>' +  } +  else { '' } +}  </%once>  <%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 ]; +}  </%init> 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.<BR><BR>'. +  qq(<A HREF="$edit_url"><I>Add a new period</I></A><BR><BR>); + +sub interval { +  my $i = shift; +  '<TABLE>' +  .join('', map { '<TR><TD>'.($_->description)[$i].'</TR></TD>' } +      shift->intervals +  ) . '</TABLE>'; +} + +# 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' ); + +</%init> 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; +} +</%init> 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') %> + +<FORM METHOD="POST" ACTION="<% "${p}edit/process/rate_time.cgi" %>"> +<INPUT TYPE="hidden" NAME="ratetimenum" VALUE="<% $ratetimenum %>"> +<% ntable('#cccccc',2) %> +<TABLE> +  <TR> +    <TH ALIGN="right">Period name</TH> +    <TD><INPUT TYPE="text" NAME="ratetimename" VALUE="<% $rate_time ? $rate_time->ratetimename : '' %>"></TD> +  </TR> +</TABLE> +<% include('/elements/auto-table.html',  +                      header => [ 'Start', 'End' ], +                      fields => [ 'stime', 'etime' ], +                      size   => [ 18, 18 ], +                      maxl   => [ 15, 15 ], +                      align  => [ 'right', 'right' ], +                      data   => \@data, +   ) %> +<INPUT TYPE="submit" VALUE="<% $rate_time ? 'Apply changes' : 'Add period'%>"> +</FORM> +<BR> +<A HREF="<% "${p}edit/process/rate_time.cgi?ratetimenum=$ratetimenum;delete=1" %>">Delete this period</A> +<% 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; +} + +</%init> 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. +</%doc> + +<TABLE ID="<% $prefix %>AutoTable" BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0> +  <TR> +% foreach (@header) { +    <TH><% $_ %></TH> +% } +  </TR> +% my $row = 0; +% for ( $row = 0; $row < scalar @data; $row++ ) { +  <TR> +%   my $col = 0; +%   for ( $col = 0; $col < scalar @fields; $col++ ) { +%     my $id = $prefix . $fields[$col] . $row; +    <TD> +      <INPUT TYPE      = "text" +             NAME      = "<% $id %>" +             ID        = "<% $id %>" +             SIZE      = <% $size[$col] %> +             MAXLENGTH = <% $maxl[$col] %> +             STYLE     = "text-align:<% $align[$col] %>" +             VALUE     = "<% $data[$row][$col] %>" +             onchange  = "possiblyAddRow();" +      > +    </TD> +%   } +    <TD> +      <IMG SRC     = "<% "${p}images/cross.png" %>"  +           ALT     = "X"  +           onclick = "deleteThisRow(this);" +           > +    </TD> +  </TR> +% } +</TABLE> + +<SCRIPT TYPE="text/javascript"> +  var <% $prefix %>rownum = <% $row %>; +  var <% $prefix %>table = document.getElementById('<% $prefix %>AutoTable'); + +  function rownum_of(obj) { +    return (obj.parentNode.parentNode.sectionRowIndex); +  } + +  function possiblyAddRow() { +    if ( <% $prefix %>rownum == rownum_of(this) ) { +      <% $prefix %>addRow(); +    } +  } + +  function <% $prefix %>addRow() { +    var row = <% $prefix %>table.insertRow(<% $prefix %>rownum + 1); +%   my $col = 0; +%   for( $col = 0; $col < scalar @fields; $col++ ) { +%     my $field = $prefix.$fields[$col]; +    var <% $field %>_cell = document.createElement('TD'); +      var <% $field %>_input = document.createElement('INPUT'); +      <% $field %>_input.setAttribute('name', '<% $field %>'+<% $prefix %>rownum); +      <% $field %>_input.setAttribute('id',   '<% $field %>'+<% $prefix %>rownum); +      <% $field %>_input.setAttribute('type', 'text'); +      <% $field %>_input.setAttribute('size', <% $size[$col] %>); +      <% $field %>_input.setAttribute('maxlength', <% $maxl[$col] %>); +      <% $field %>_input.style.textAlign = '<% $align[$col] %>'; +      <% $field %>_input.onchange = possiblyAddRow; +      <% $field %>_cell.appendChild(<% $field %>_input); +    row.appendChild(<% $field %>_cell); +%   } +    var delcell = document.createElement('TD'); +      var delinput = document.createElement('IMG'); +      delinput.setAttribute('src', '<% "${p}images/cross.png" %>'); +      delinput.setAttribute('alt', 'X'); +      delinput.setAttribute('onclick', 'deleteThisRow(this);'); +      delcell.appendChild(delinput); +    row.appendChild(delcell); + +    <% $prefix %>rownum++; +  } + +  function deleteThisRow(obj) { +    if(<% $prefix %>rownum == rownum_of(obj))  { +      <% $prefix %>addRow(); +    } +    <% $prefix %>table.deleteRow(rownum_of(obj)); +    <% $prefix %>rownum--; +    return(false); +  } + +  <% $prefix %>addRow(); +</SCRIPT> + +<%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); + +</%init> 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') %> +    <SCRIPT TYPE="text/javascript"> +      window.top.location.reload(); +    </SCRIPT> +    </BODY></HTML> +% } +<%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; + +</%init> | 
