diff options
| author | ivan <ivan> | 2011-11-14 04:31:32 +0000 | 
|---|---|---|
| committer | ivan <ivan> | 2011-11-14 04:31:32 +0000 | 
| commit | bda8c33f9b346ba6cd7aa4174ce0d3e37db7bd49 (patch) | |
| tree | 6e2ecb77cf8321e2a8ff16d32204b2e04befb369 | |
| parent | c5ceb74497f48162185959070d841a91e8bb58d9 (diff) | |
rate tiers for vnes, RT#14903
| -rw-r--r-- | FS/FS.pm | 4 | ||||
| -rw-r--r-- | FS/FS/Schema.pm | 53 | ||||
| -rw-r--r-- | FS/FS/cdr.pm | 30 | ||||
| -rw-r--r-- | FS/FS/cust_bill_pkg.pm | 23 | ||||
| -rw-r--r-- | FS/FS/part_pkg/voip_inbound.pm | 55 | ||||
| -rw-r--r-- | FS/FS/part_pkg/voip_tiered.pm | 258 | ||||
| -rw-r--r-- | FS/FS/rate_tier.pm | 153 | ||||
| -rw-r--r-- | FS/FS/rate_tier_detail.pm | 139 | ||||
| -rw-r--r-- | FS/MANIFEST | 4 | ||||
| -rw-r--r-- | FS/t/rate_tier.t | 5 | ||||
| -rw-r--r-- | FS/t/rate_tier_detail.t | 5 | ||||
| -rw-r--r-- | httemplate/browse/rate_tier.html | 53 | ||||
| -rw-r--r-- | httemplate/edit/elements/edit.html | 2 | ||||
| -rw-r--r-- | httemplate/edit/process/rate_tier.html | 15 | ||||
| -rw-r--r-- | httemplate/edit/rate_tier.html | 54 | ||||
| -rw-r--r-- | httemplate/elements/menu.html | 4 | ||||
| -rw-r--r-- | httemplate/elements/rate_tier_detail.html | 66 | ||||
| -rw-r--r-- | httemplate/elements/tr-rate_tier_detail.html | 24 | 
18 files changed, 893 insertions, 54 deletions
| @@ -250,6 +250,10 @@ L<FS::rate_prefix> - Rate region prefixes for call billing  L<FS::rate_detail> - Rate plan detail for call billing +L<FS::rate_tier> - Rate tiers for call billing + +L<FS::rate_tier_details> - Rater tier details for call billing +  L<FS::usage_class> - Usage class class  L<FS::agent> - Agent (reseller) class diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 87bdc2647..0568bdf1d 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -2688,6 +2688,30 @@ sub tables_hashref {        'index'       => [],      }, +    #not really part of the above rate_ stuff (used with flat rate rather than +    # rated billing), but could be eventually, and its a rate +    'rate_tier' => { +      'columns' => [ +        'tiernum',   'serial', '',      '', '', '', +        'tiername', 'varchar', '', $char_d, '', '', +      ], +      'primary_key' => 'tiernum', +      'unique'      => [ [ 'tiername'], ], +      'index'       => [], +    }, + +    'rate_tier_detail' => { +      'columns' => [ +        'tierdetailnum', 'serial', '',     '', '', '', +        'tiernum',          'int', '',     '', '', '', +        'min_quan',         'int', '',     '', '', '', +        'min_charge',   'decimal', '', '10,4', '', '', +      ], +      'primary_key' => 'tierdetailnum', +      'unique'      => [], +      'index'       => [ ['tiernum'], ], +    }, +      'usage_class' => {        'columns' => [          'classnum',    'serial',      '',      '', '', '',  @@ -2875,24 +2899,17 @@ sub tables_hashref {          'max_callers', 'int',  'NULL',      '',    '', '',          ### -        # fields for unitel/RSLCOM/convergent that don't map well to asterisk -        # defaults -        # though these are now used elsewhere: +        # old fields for unitel/RSLCOM/convergent that don't map to asterisk +        # ones we adoped moved to "own fields" section below          # charged_party, upstream_price, rated_price, carrierid, cdrtypenum          ### -        #cdr_type: Usage = 1, S&E = 7, OC&C = 8 -        'cdrtypenum',              'int', 'NULL',      '', '', '', - -        'charged_party',       'varchar', 'NULL', $char_d, '', '', -          'upstream_currency',      'char', 'NULL',       3, '', '',          'upstream_price',      'decimal', 'NULL',  '10,4', '', '',           'upstream_rateplanid',     'int', 'NULL',      '', '', '', #?          # how it was rated internally...          'ratedetailnum',           'int', 'NULL',      '', '', '', -        'rated_price',         'decimal', 'NULL',  '10,4', '', '',          'distance',            'decimal', 'NULL',      '', '', '',          'islocal',                 'int', 'NULL',      '', '', '', # '',  '', 0, '' instead? @@ -2903,16 +2920,24 @@ sub tables_hashref {          'description',         'varchar', 'NULL', $char_d, '', '',          'quantity',                'int', 'NULL',      '', '', '',  -        #cdr_carrier: Telstra =1, Optus = 2, RSL COM = 3 -        'carrierid',               'int', 'NULL',      '', '', '', -          'upstream_rateid',         'int', 'NULL',      '', '', '',          ###          #and now for our own fields          ### -        # a svcnum... right..? +        'cdrtypenum',              'int', 'NULL',      '', '', '', + +        'charged_party',       'varchar', 'NULL', $char_d, '', '', + +        # how it was rated internally... +        'rated_price',         'decimal', 'NULL',  '10,4', '', '', +        'rated_seconds',           'int', 'NULL',      '', '', '', +        'rated_minutes', 'double precision', 'NULL',   '', '', '', + +        'carrierid',               'int', 'NULL',      '', '', '', + +        # service it was matched to          'svcnum',             'int',   'NULL',     '',   '', '',           #NULL, done (or something) @@ -2963,6 +2988,8 @@ sub tables_hashref {          'acctid',        'bigint',     '',      '', '', '',           'termpart',         'int',     '',      '', '', '',#future use see below          'rated_price',  'decimal', 'NULL',  '10,4', '', '', +        'rated_seconds',    'int', 'NULL',      '', '', '', +        'rated_minutes', 'double precision', 'NULL',   '', '', '',          'status',       'varchar', 'NULL',      32, '', '',          'svcnum',           'int', 'NULL',      '', '', '',        ], diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm index 36721a81a..850f797bc 100644 --- a/FS/FS/cdr.pm +++ b/FS/FS/cdr.pm @@ -401,13 +401,15 @@ error, otherwise returns false.  sub set_status_and_rated_price {    my($self, $status, $rated_price, $svcnum, %opt) = @_; -  if($opt{'inbound'}) { + +  if ($opt{'inbound'}) { +      my $term = qsearchs('cdr_termination', {          acctid   => $self->acctid,           termpart => 1 # inbound      });      my $error; -    if($term) { +    if ( $term ) {        warn "replacing existing cdr status (".$self->acctid.")\n" if $term;        $error = $term->delete;        return $error if $error; @@ -419,13 +421,19 @@ sub set_status_and_rated_price {          status      => $status,          svcnum      => $svcnum,      }); +    $term->rated_seconds($opt{rated_seconds}) if exists($opt{rated_seconds}); +    $term->rated_minutes($opt{rated_minutes}) if exists($opt{rated_minutes});      return $term->insert; -  } -  else { + +  } else { +      $self->freesidestatus($status);      $self->rated_price($rated_price); +    $self->rated_seconds($opt{rated_seconds}) if exists($opt{rated_seconds}); +    $self->rated_minutes($opt{rated_minutes}) if exists($opt{rated_minutes});      $self->svcnum($svcnum) if $svcnum;      return $self->replace(); +    }  } @@ -642,6 +650,20 @@ sub export_formats {    return %export_formats;  } +=item downstream_csv OPTION => VALUE ... + +Options: + +format + +charge + +seconds + +granularity + +=cut +  sub downstream_csv {    my( $self, %opt ) = @_; diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index c9b0a4dc1..52b29f2d3 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -820,11 +820,21 @@ sub usage {    if ( $self->get('details') ) {      @values =  -      map { $_->[2] } -      grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) } +      map { ref($_) eq 'HASH' +              ? $_->{'amount'} +              : $_->[2]  +          } +      grep { ref($_) && ( defined($classnum) +                            ? $classnum eq ( ref($_) eq 'HASH' +                                               ? $_->{'classnum'} +                                               : $_->[3] +                                           ) +                            : 1 +                        ) +           }        @{ $self->get('details') }; -  }else{ +  } else {      my $hashref = { 'billpkgnum' => $self->billpkgnum };      $hashref->{ 'classnum' } = $classnum if defined($classnum); @@ -852,11 +862,14 @@ sub usage_classes {      my %seen = ();      foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) { -      $seen{ $detail->[3] } = 1; +      $seen{ ref($detail) eq 'HASH' +               ? $detail->{'classnum'} +               : $detail->[3] +           } = 1;      }      keys %seen; -  }else{ +  } else {      map { $_->classnum }          qsearch({ table   => 'cust_bill_pkg_detail', diff --git a/FS/FS/part_pkg/voip_inbound.pm b/FS/FS/part_pkg/voip_inbound.pm index e3af5aaf5..4714ccb28 100644 --- a/FS/FS/part_pkg/voip_inbound.pm +++ b/FS/FS/part_pkg/voip_inbound.pm @@ -1,16 +1,15 @@  package FS::part_pkg::voip_inbound; +use base qw( FS::part_pkg::recur_Common );  use strict; -use vars qw(@ISA $DEBUG %info); +use vars qw($DEBUG %info);  use Date::Format;  use Tie::IxHash; +use Text::CSV_XS;  use FS::Conf;  use FS::Record qw(qsearchs qsearch); -use FS::part_pkg::recur_Common;  use FS::cdr; -use FS::part_pkg::recur_Common; - -@ISA = qw(FS::part_pkg::recur_Common); +use FS::rate_detail;  $DEBUG = 0; @@ -56,10 +55,6 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities();                            'default' => '+1',                          }, -    'disable_tollfree' => { 'name' => 'Disable automatic toll-free processing', -                            'type' => 'checkbox', -                          }, -      'use_amaflags' => { 'name' => 'Only charge for CDRs where the amaflags field is set to "2" ("BILL"/"BILLING").',                          'type' => 'checkbox',                        }, @@ -148,7 +143,6 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities();                         FS::part_pkg::prorate_Mixin::fieldorder,                     qw( min_charge min_included sec_granularity                         default_prefix -                       disable_tollfree                         use_amaflags                         use_carrierid                         use_cdrtypenum ignore_cdrtypenum @@ -160,7 +154,7 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities();                         bill_every_call                       )                    ], -  'weight' => 40, +  'weight' => 42,  );  sub price_info { @@ -205,25 +199,21 @@ sub calc_usage {    my $spool_cdr = $cust_pkg->cust_main->spool_cdr; -  my $included_min = ($self->option('min_included')  -                        && $self->option('min_included') > 0)  -                                        ? $self->option('min_included') : 0;    my $charges = 0;  #  my $downstream_cdr = ''; -  my $disable_tollfree  = $self->option('disable_tollfree'); -  my $ignore_unrateable = $self->option('ignore_unrateable', 'Hush!'); -  my $use_duration      = $self->option('use_duration'); - -  my $output_format     = $self->option('output_format', 'Hush!') || 'default'; +  my $included_min  = $self->option('min_included', 1) || 0; +  my $use_duration  = $self->option('use_duration'); +  my $output_format = $self->option('output_format', 1) || 'default'; +  my $granularity   = length($self->option('sec_granularity')) +                        ? $self->option('sec_granularity') +                        : 60;    #for check_chargable, so we don't keep looking up options inside the loop    my %opt_cache = (); -  eval "use Text::CSV_XS;"; -  die $@ if $@;    my $csv = new Text::CSV_XS;    foreach my $cust_svc ( @@ -232,19 +222,25 @@ sub calc_usage {      my $svc_phone = $cust_svc->svc_x;      foreach my $cdr ( $svc_phone->get_cdrs( -      'for_update'     => 1, -      'status'         => '', # unprocessed only -      'default_prefix' => $self->option('default_prefix'),        'inbound'        => 1, +      'default_prefix' => $self->option('default_prefix'), +      'status'         => '', # unprocessed only +      'for_update'     => 1,        )      ) { + +      my $reason = $self->check_chargable( $cdr, +                                           'option_cache' => \%opt_cache, +                                         ); +      if ( $reason ) { +        warn "not charging for CDR ($reason)\n" if $DEBUG; +        next; +      } +        if ( $DEBUG > 1 ) {          warn "rating inbound CDR $cdr\n".               join('', map { "  $_ => ". $cdr->{$_}. "\n" } keys %$cdr );        } -      my $granularity = length($self->option('sec_granularity')) -                          ? $self->option('sec_granularity') -                          : 60;        my $seconds = $use_duration ? $cdr->duration : $cdr->billsec; @@ -287,7 +283,7 @@ sub calc_usage {                $call_details[0],                $charge,                $cdr->calltypenum, #classnum -              $self->phonenum, +              '', #phonenum,                $cdr->accountcode,                $cdr->startdate,                $seconds, @@ -353,8 +349,7 @@ sub check_chargable {    return "carrierid != $opt{'use_carrierid'}"      if length($opt{'use_carrierid'}) -    && $cdr->carrierid ne $opt{'use_carrierid'} #ne otherwise 0 matches '' -    && ! $flags{'da_rewrote'}; +    && $cdr->carrierid ne $opt{'use_carrierid'}; #ne otherwise 0 matches ''    return "cdrtypenum != $opt{'use_cdrtypenum'}"      if length($opt{'use_cdrtypenum'}) diff --git a/FS/FS/part_pkg/voip_tiered.pm b/FS/FS/part_pkg/voip_tiered.pm new file mode 100644 index 000000000..29e60d499 --- /dev/null +++ b/FS/FS/part_pkg/voip_tiered.pm @@ -0,0 +1,258 @@ +package FS::part_pkg::voip_tiered; +use base qw( FS::part_pkg::voip_cdr ); + +use strict; +use vars qw( $DEBUG %info ); +use Tie::IxHash; +use Date::Format; +use Text::CSV_XS; +use FS::Conf; +use FS::Record qw(qsearchs); # qsearch); +use FS::cdr; +use FS::rate_tier; +use FS::rate_detail; + +use Data::Dumper; + +$DEBUG = 0; + +tie my %cdr_inout, 'Tie::IxHash', +  'outbound'         => 'Outbound', +  'inbound'          => 'Inbound', +  'outbound_inbound' => 'Outbound and Inbound', +; + +tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities(); + +%info = ( +  'name' => 'VoIP tiered rate pricing of CDRs', +  'shortname' => 'VoIP/telco CDR tiered rating', +  'inherit_fields' => [ 'voip_cdr', 'prorate_Mixin', 'global_Mixin' ], +  'fields' => { +    'tiernum' => { 'name' => 'Tier plan', +                   'type' => 'select', +                   'select_table' => 'rate_tier', +                   'select_key'   => 'tiernum', +                   'select_label' => 'tiername', +                 }, +    'cdr_inout' => { 'name'=> 'Call direction when using phone number matching', +                     'type'=> 'select', +                     'select_options' => \%cdr_inout, +                   }, +    'min_included' => { 'name' => 'Minutes included', +                    }, +    'sec_granularity' => { 'name' => 'Granularity', +                           'type' => 'select', +                           'select_options' => \%granularity, +                         }, +    'rating_method'                          => { 'disabled' => 1 }, +    'ratenum'                                => { 'disabled' => 1 }, +    'intrastate_ratenum'                     => { 'disabled' => 1 }, +    'min_charge'                             => { 'disabled' => 1 }, +    'ignore_unrateable'                      => { 'disabled' => 1 }, +    'domestic_prefix'                        => { 'disabled' => 1 }, +    'international_prefix'                   => { 'disabled' => 1 }, +    'disable_tollfree'                       => { 'disabled' => 1 }, +    'noskip_src_length_accountcode_tollfree' => { 'disabled' => 1 }, +    'accountcode_tollfree_ratenum'           => { 'disabled' => 1 }, +    'noskip_dst_length_accountcode_tollfree' => { 'disabled' => 1 }, +  }, +  'fieldorder' => [qw( +                       recur_temporality +                       recur_method cutoff_day ), +                       FS::part_pkg::prorate_Mixin::fieldorder, +                   qw( +                       cdr_svc_method cdr_inout +                       tiernum +                     ) +                  ], +  'weight' => 44, +); + +sub calc_usage { +  my $self = shift; +  my($cust_pkg, $sdate, $details, $param ) = @_; + +  #my $last_bill = $cust_pkg->last_bill; +  my $last_bill = $cust_pkg->get('last_bill'); #->last_bill falls back to setup + +  return 0 +    if $self->recur_temporality eq 'preceding' +    && ( $last_bill eq '' || $last_bill == 0 ); + +  my $included_min    = $self->option('min_included', 1) || 0; +  my $cdr_svc_method  = $self->option('cdr_svc_method',1)||'svc_phone.phonenum'; +  my $cdr_inout       = ($cdr_svc_method eq 'svc_phone.phonenum') +                          && $self->option('cdr_inout',1) +                          || 'outbound'; +  my $use_duration    = $self->option('use_duration'); +  my $granularity     = length($self->option('sec_granularity')) +                          ? $self->option('sec_granularity') +                          : 60; + +  #for check_chargable, so we don't keep looking up options inside the loop +  my %opt_cache = (); + +  my($svc_table, $svc_field) = split('\.', $cdr_svc_method); + +  my %options = ( +    'disable_src'    => $self->option('disable_src'), +    'default_prefix' => $self->option('default_prefix'), +    'status'         => '', +    'for_update'     => 1, +  );  # $last_bill, $$sdate ) +  $options{'by_svcnum'} = 1 if $svc_field eq 'svcnum'; + +  ### +  # pass one: find the total minutes/calls and store the CDRs +  ### +  my $total = 0; +  my @cdrs = (); + +  my @cust_svc; +  if( $self->option('bill_inactive_svcs',1) ) { +    #XXX in this mode do we need to restrict the set of CDRs by date also? +    @cust_svc = $cust_pkg->h_cust_svc($$sdate, $last_bill); +  } else { +    @cust_svc = $cust_pkg->cust_svc; +  } +  @cust_svc = grep { $_->part_svc->svcdb eq $svc_table } @cust_svc; + +  foreach my $cust_svc (@cust_svc) { + +    my $svc_x; +    if( $self->option('bill_inactive_svcs',1) ) { +      $svc_x = $cust_svc->h_svc_x($$sdate, $last_bill); +    } +    else { +      $svc_x = $cust_svc->svc_x; +    } + +    foreach my $pass (split('_', $cdr_inout)) { + +      $options{'inbound'} = ( $pass eq 'inbound' ); + +      foreach my $cdr ( +        $svc_x->get_cdrs( %options ) +      ) { +        if ( $DEBUG > 1 ) { +          warn "rating CDR $cdr\n". +               join('', map { "  $_ => ". $cdr->{$_}. "\n" } keys %$cdr ); +        } + +        my $charge = ''; +        my $seconds = ''; + +        $seconds = $use_duration ? $cdr->duration : $cdr->billsec; + +        $seconds += $granularity - ( $seconds % $granularity ) +          if $seconds      # don't granular-ize 0 billsec calls (bills them) +          && $granularity  # 0 is per call +          && $seconds % $granularity; +        my $minutes = $granularity ? ($seconds / 60) : 1; + +        my $charge_min = $minutes; + +        $included_min -= $minutes; +        if ( $included_min > 0 ) { +          $charge_min = 0; +        } else { +           $charge_min = 0 - $included_min; +           $included_min = 0; +        } + +        $cdr->tmp_inout( $pass ); +        $cdr->tmp_rated_seconds( $seconds ); +        $cdr->tmp_rated_minutes( $charge_min ); +        $cdr->tmp_svcnum( $cust_svc->svcnum ); +        push @cdrs, $cdr; +        $total += $charge_min; + +      } # $cdr + +    } # $pass +  +  } # $cust_svc + +  ### +  # pass two: find a tiered rate and do the rest +  ### + +  my $rate_tier = qsearchs('rate_tier', { tiernum=>$self->option('tiernum') } ) +    or die "unknown tiernum ". $self->option('tiernum'); +  my $rate_tier_detail = $rate_tier->rate_tier_detail( $total ) +    or die "no base rate for tier? ($total)"; +  my $min_charge = $rate_tier_detail->min_charge; + +  my $output_format = $self->option('output_format', 'Hush!') || 'default'; + +  my $csv = new Text::CSV_XS; + +  my $charges = 0; +  my @invoice_details_sort; + +  foreach my $cdr (@cdrs) { + +    my $charge_min = $cdr->tmp_rated_minutes; + +    my $charge = sprintf('%.4f', ( $min_charge * $charge_min ) +                                 + 0.0000000001 ); #so 1.00005 rounds to 1.0001 + + +    if ( $charge > 0 ) { +      $charges += $charge; + +      my $detail =  +        $cdr->downstream_csv( 'format'  => $output_format, +                              'charge'  => $charge, +                              'seconds' => ($use_duration ?  +                                              $cdr->duration :  +                                              $cdr->billsec), +                              'granularity' => $granularity, +                            ); + +      my $call_details = +        { format      => 'C', +          detail      => $detail, +          amount      => $charge, +          #classnum    => $cdr->calltypenum, #classnum +          #phonenum    => $phonenum, #XXX need this to sort on them +          accountcode => $cdr->accountcode, +          startdate   => $cdr->startdate, +          duration    => $cdr->tmp_rated_seconds, +        }; + +       #warn "  adding details on charge to invoice: [ ". +      #    join(', ', @{$call_details} ). " ]" +      #  if ( $DEBUG && ref($call_details) ); +      push @invoice_details_sort, [ $call_details, $cdr->calldate_unix ]; +    } + +    my $error = $cdr->set_status_and_rated_price( +      'done', +      $charge, +      $cdr->tmp_svcnum, +      'inbound'       => ($cdr->tmp_inout eq 'inbound'), +      'rated_minutes' => $charge_min, +      'rated_seconds' => $cdr->tmp_rated_seconds, +    ); +    die $error if $error; + + +  } + +  my @sorted_invoice_details = sort { ${$a}[1] <=> ${$b}[1] } @invoice_details_sort; +  foreach my $sorted_call_detail ( @sorted_invoice_details ) { +      push @$details, ${$sorted_call_detail}[0]; +  } + +  unshift @$details, { format => 'C', +                       detail => FS::cdr::invoice_header($output_format), +                     } +    if @$details; + +  $charges; +} + +1; + diff --git a/FS/FS/rate_tier.pm b/FS/FS/rate_tier.pm new file mode 100644 index 000000000..1ca457d63 --- /dev/null +++ b/FS/FS/rate_tier.pm @@ -0,0 +1,153 @@ +package FS::rate_tier; +use base qw( FS::o2m_Common FS::Record ); + +use strict; +use FS::Record qw( qsearch qsearchs ); +use FS::rate_tier_detail; + +=head1 NAME + +FS::rate_tier - Object methods for rate_tier records + +=head1 SYNOPSIS + +  use FS::rate_tier; + +  $record = new FS::rate_tier \%hash; +  $record = new FS::rate_tier { 'column' => 'value' }; + +  $error = $record->insert; + +  $error = $new_record->replace($old_record); + +  $error = $record->delete; + +  $error = $record->check; + +=head1 DESCRIPTION + +An FS::rate_tier object represents a set of rate tiers.  FS::rate_tier inherits + from FS::Record.  The following fields are currently supported: + +=over 4 + +=item tiernum + +primary key + +=item tiername + +tiername + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new record.  To add the record 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_tier'; } + +=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 record.  If there is +an error, returns the error, otherwise returns false.  Called by the insert +and replace methods. + +=cut + +# the check method should currently be supplied - FS::Record contains some +# data checking routines + +sub check { +  my $self = shift; + +  my $error =  +    $self->ut_numbern('tiernum') +    || $self->ut_text('tiername') +  ; +  return $error if $error; + +  $self->SUPER::check; +} + +=item rate_tier_detail QUANTITY + +=cut + +sub rate_tier_detail { +  my $self = shift; + +  if ( defined($_[0]) && length($_[0]) ) { + +    my $quantity = shift; + +    qsearchs({ +      'table'    => 'rate_tier_detail', +      'hashref'  => { 'tiernum'  => $self->tiernum, +                      'min_quan' => { op=>'<=', value=>$quantity }, +                    }, +      'order_by' => 'ORDER BY min_charge ASC LIMIT 1', +    }); + +  } else { + +    qsearch({ +      'table'    => 'rate_tier_detail', +      'hashref'  => { 'tiernum' => $self->tiernum, }, +      'order_by' => 'ORDER BY min_quan ASC', +    }); + +  } + +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/rate_tier_detail.pm b/FS/FS/rate_tier_detail.pm new file mode 100644 index 000000000..60896f489 --- /dev/null +++ b/FS/FS/rate_tier_detail.pm @@ -0,0 +1,139 @@ +package FS::rate_tier_detail; +use base qw( FS::Record ); + +use strict; +use FS::Record; # qw( qsearch qsearchs ); +use FS::rate_tier; + +=head1 NAME + +FS::rate_tier_detail - Object methods for rate_tier_detail records + +=head1 SYNOPSIS + +  use FS::rate_tier_detail; + +  $record = new FS::rate_tier_detail \%hash; +  $record = new FS::rate_tier_detail { 'column' => 'value' }; + +  $error = $record->insert; + +  $error = $new_record->replace($old_record); + +  $error = $record->delete; + +  $error = $record->check; + +=head1 DESCRIPTION + +An FS::rate_tier_detail object represents rate tier pricing. +FS::rate_tier_detail inherits from FS::Record.  The following fields are +currently supported: + +=over 4 + +=item tierdetailnum + +primary key + +=item tiernum + +tiernum + +=item min_quan + +min_quan + +=item min_charge + +min_charge + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new record.  To add the record 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_tier_detail'; } + +=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 record.  If there is +an error, returns the error, otherwise returns false.  Called by the insert +and replace methods. + +=cut + +# the check method should currently be supplied - FS::Record contains some +# data checking routines + +sub check { +  my $self = shift; + +  my $min_quan = $self->min_quan; +  $min_quan =~ s/[ ,]//g; +  $self->min_quan($min_quan); + +  $self->min_quan(0) if $self->min_quan eq ''; + +  my $error =  +    $self->ut_numbern('tierdetailnum') +    || $self->ut_foreign_key('tiernum', 'rate_tier', 'tiernum') +    || $self->ut_number('min_quan') +    || $self->ut_textn('min_charge') #XXX money?  but we use 4 decimal places +  ; +  return $error if $error; + +  $self->SUPER::check; +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/MANIFEST b/FS/MANIFEST index 37f1a5896..e983ea207 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -616,3 +616,7 @@ FS/export_nas.pm  t/export_nas.t  FS/legacy_cust_bill.pm  t/legacy_cust_bill.t +FS/rate_tier.pm +t/rate_tier.t +FS/rate_tier_detail.pm +t/rate_tier_detail.t diff --git a/FS/t/rate_tier.t b/FS/t/rate_tier.t new file mode 100644 index 000000000..d735bdbb8 --- /dev/null +++ b/FS/t/rate_tier.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::rate_tier; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/rate_tier_detail.t b/FS/t/rate_tier_detail.t new file mode 100644 index 000000000..eccd676ae --- /dev/null +++ b/FS/t/rate_tier_detail.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::rate_tier_detail; +$loaded=1; +print "ok 1\n"; diff --git a/httemplate/browse/rate_tier.html b/httemplate/browse/rate_tier.html new file mode 100644 index 000000000..d84080857 --- /dev/null +++ b/httemplate/browse/rate_tier.html @@ -0,0 +1,53 @@ +<% include( 'elements/browse.html', +              'title'         => 'Tiering plans', +              'name_singular' => 'tiering plan', +              'menubar'       => [ 'Add a new tier plan' => +                                     $p.'edit/rate_tier.html', +                                 ], +              'query'         => { 'table' => 'rate_tier', }, +              'count_query'   => 'SELECT COUNT(*) FROM rate_tier', +              'header'        => [ 'Plan', 'Tiers', ], +              'fields'        => [ 'tiername', +                                   $details_sub, +                                 ], +              'links'         => [ $link, ], +              #'disableable'   => 1, +              #'disabled_statuspos' => 1, +          ) +%> +<%init> + +die "access denied" +  unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +my $link = [ "${p}edit/rate_tier.html?", 'tiernum' ]; + +my $details_sub = sub { +  my $rate_tier = shift; + +  [ [ { 'data'  => 'Minimum quantity', +        'align' => 'center', +      }, +      { 'data'  => 'Charge per minute/call', +        'align' => 'center', +      }, +    ], +    map { my $rate_tier_detail = $_; +          [ +            { 'data'  => $rate_tier_detail->min_quan, +              'align' => 'right', +            }, +            { 'data'  => $money_char. $rate_tier_detail->min_charge, +              'align' => 'right', +            }, +          ]; +        } +      $rate_tier->rate_tier_detail +  ]; + +}; + +</%init> diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html index 6db54fd48..4e896f239 100644 --- a/httemplate/edit/elements/edit.html +++ b/httemplate/edit/elements/edit.html @@ -549,7 +549,7 @@ Example:            // only spawn if we're the last element... return if not -          var field_regex = /(\d+)(_[a-z]+)?$/; +          var field_regex = /(\d+)(_[a-z_]+)?$/;            var match = field_regex.exec(what.name);            if ( !match ) {              alert(what.name + " didn't match for " + what); diff --git a/httemplate/edit/process/rate_tier.html b/httemplate/edit/process/rate_tier.html new file mode 100644 index 000000000..f29edbb50 --- /dev/null +++ b/httemplate/edit/process/rate_tier.html @@ -0,0 +1,15 @@ +<% include( 'elements/process.html', +               'table'       => 'rate_tier', +               'viewall_dir' => 'browse', +               'process_o2m' => { +                 'table'  => 'rate_tier_detail', +                 'fields' => [qw( min_quan min_charge )], +               }, +           ) +%> +<%init> + +die "access denied" +  unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +</%init> diff --git a/httemplate/edit/rate_tier.html b/httemplate/edit/rate_tier.html new file mode 100644 index 000000000..f9df4b5ce --- /dev/null +++ b/httemplate/edit/rate_tier.html @@ -0,0 +1,54 @@ +<% include( 'elements/edit.html', +              'table'         => 'rate_tier', +              'name_singular' => 'tiering plan', +              'fields'        => [ +                'tiername', +                #{ field=>'disabled', type=>'checkbox', value=>'Y' }, +                { 'field'             => 'tierdetailnum', +                  'type'              => 'rate_tier_detail', +                  'colspan'           => 2, +                  'o2m_table'         => 'rate_tier_detail', +                  'm2_label'          => 'Tier', +                  'm2_error_callback' => $m2_error_callback, +                }, +              ], +              'labels'        => { 'tiernum'       => 'Plan #', +                                   'tiername'      => 'Tiering plan', +                                   'tierdetailnum' => 'Tier', +                                 }, +              'viewall_dir' => 'browse', +          ) +%> +<%init> + +die "access denied" +  unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $m2_error_callback = sub { +  my($cgi, $object) = @_; + +  #process_o2m fields in process/rate_tier.html +  my @fields = qw( min_quan min_charge ); +  my @gfields = ( '', map "_$_", @fields ); + +  map { +        if ( /^tierdetailnum(\d+)$/ ) { +          my $num = $1; +          if ( grep $cgi->param("tierdetailnum$num$_"), @gfields ) { +            my $x = new FS::rate_tier_detail { +              'tierdetailnum' => scalar($cgi->param("tierdetailnum$num")), +              map { $_ => scalar($cgi->param("tierdetailnum${num}_$_")) } +                  @fields, +            }; +            $x; +          } else { +            (); +          } +        } else { +          (); +        } +      } +      $cgi->param; +}; + +</%init> diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index 17c422963..6bfe712f8 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -25,7 +25,7 @@  %                           'style'       => 'color:#999999',  %                           'actionlabel' => emt('About'),  %                           'width'       => 300, -%                           'height'      => 360, +%                           'height'      => 375,  %                           'color'       => '#7e0079',  %                           'scrolling'   => 'no',  %                       ); @@ -473,6 +473,8 @@ tie my %config_billing_rates, 'Tie::IxHash',    'Usage classes'  => [ $fsurl.'browse/usage_class.html', 'Usage classes define groups of usage for taxation.' ],    'Time periods' => [ $fsurl.'browse/rate_time.html', 'Time periods define days and hours for rate plans' ],    'Edit rates with Excel' => [ $fsurl.'misc/rate_edit_excel.html', 'Download and edit rates with Excel, then upload changes.' ], #"Edit with Excel" ? +  'separator'     => '', #its a separator! +  'Tiering plans' => [ $fsurl.'browse/rate_tier.html', 'Rating tiers' ],  ;  tie my %config_billing, 'Tie::IxHash'; diff --git a/httemplate/elements/rate_tier_detail.html b/httemplate/elements/rate_tier_detail.html new file mode 100644 index 000000000..ef1f38b8a --- /dev/null +++ b/httemplate/elements/rate_tier_detail.html @@ -0,0 +1,66 @@ +% unless ( $opt{'js_only'} ) { + +  <INPUT TYPE="hidden" NAME="<%$name%>" ID="<%$id%>" VALUE="<% $curr_value %>"> + +  <TABLE> +    <TR> +%     foreach my $field ( @fields ) { +% +%       my $value = $rate_tier_detail->get($field); + +        <TD> +          <% $field eq 'min_charge' ? $money_char : '' %> +          <INPUT TYPE  = "text" +                 NAME  = "<%$name%>_<%$field%>" +                 ID    = "<%$id%>_<%$field%>" +                 SIZE  = "<% $size{$field} || 15 %>" +                 STYLE = "text-align:right" +                 VALUE = "<% scalar($cgi->param($name."_$field")) +                             || $value |h %>" +                 <% $onchange %> +          ><BR> +          <FONT SIZE="-1"><% $label{$field} %></FONT> +        </TD> +%     } +    </TR> +  </TABLE> + +% } +<%init> + +my( %opt ) = @_; + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +my $name = $opt{'element_name'} || $opt{'field'} || 'tierdetailnum'; +my $id = $opt{'id'} || 'tierdetailnum'; + +my $curr_value = $opt{'curr_value'} || $opt{'value'}; + +my $onchange = ''; +if ( $opt{'onchange'} ) { +  $onchange = $opt{'onchange'}; +  $onchange .= '(this)' unless $onchange =~ /\(\w*\);?$/; +  $onchange =~ s/\(what\);/\(this\);/g; #ugh, terrible hack.  all onchange +                                        #callbacks should act the same +  $onchange = 'onChange="'. $onchange. '"'; +} + +my $rate_tier_detail; +if ( $curr_value ) { +  $rate_tier_detail = qsearchs('rate_tier_detail', { 'tierdetailnum' => $curr_value } ); +} else { +  $rate_tier_detail = new FS::rate_tier_detail {}; +} + +my %size = ( 'title' => 12 ); + +tie my %label, 'Tie::IxHash', +  'min_quan'   => 'Minimum quantity', +  'min_charge' => 'Charge per minute/call', +; + +my @fields = keys %label; + +</%init> diff --git a/httemplate/elements/tr-rate_tier_detail.html b/httemplate/elements/tr-rate_tier_detail.html new file mode 100644 index 000000000..7b6f26b57 --- /dev/null +++ b/httemplate/elements/tr-rate_tier_detail.html @@ -0,0 +1,24 @@ +%   unless ( $opt{'js_only'} ) { + +      <% include('tr-td-label.html', %opt) %> +        <TD <% $cell_style %>> + +%   } +% +            <% include( '/elements/rate_tier_detail.html', %opt ) %> +% +%   unless ( $opt{'js_only'} ) { + +        </TD> +      </TR> + +%   } +<%init> + +my( %opt ) = @_; + +my $cell_style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : ''; + +$opt{'label'} ||= 'Tier'; + +</%init> | 
