agent commission schedules for consecutive invoices, #71217
authorMark Wells <mark@freeside.biz>
Tue, 2 Aug 2016 18:41:51 +0000 (11:41 -0700)
committerMark Wells <mark@freeside.biz>
Tue, 2 Aug 2016 20:17:07 +0000 (13:17 -0700)
15 files changed:
FS/FS/Mason.pm
FS/FS/Schema.pm
FS/FS/commission_rate.pm [new file with mode: 0644]
FS/FS/commission_schedule.pm [new file with mode: 0644]
FS/FS/cust_credit.pm
FS/FS/part_event/Action/bill_agent_credit_schedule.pm [new file with mode: 0644]
FS/MANIFEST
FS/t/commission_rate.t [new file with mode: 0644]
FS/t/commission_schedule.t [new file with mode: 0644]
httemplate/browse/commission_schedule.html [new file with mode: 0644]
httemplate/edit/commission_schedule.html [new file with mode: 0644]
httemplate/edit/process/commission_schedule.html [new file with mode: 0644]
httemplate/elements/commission_rate.html [new file with mode: 0644]
httemplate/elements/menu.html
httemplate/elements/tr-select-reason.html

index 1008fd5..245bdea 100644 (file)
@@ -413,6 +413,8 @@ if ( -e $addl_handler_use_file ) {
   use FS::olt_site;
   use FS::access_user_page_pref;
   use FS::part_svc_msgcat;
+  use FS::commission_schedule;
+  use FS::commission_rate;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
index ac58510..8661c4b 100644 (file)
@@ -1361,6 +1361,7 @@ sub tables_hashref {
         'commission_agentnum', 'int', 'NULL', '', '', '', #
         'commission_salesnum', 'int', 'NULL', '', '', '', #
         'commission_pkgnum',   'int', 'NULL', '', '', '', #
+        'commission_invnum',   'int', 'NULL', '', '', '',
         'credbatch',    'varchar', 'NULL', $char_d, '', '',
       ],
       'primary_key'  => 'crednum',
@@ -1396,6 +1397,10 @@ sub tables_hashref {
                             table      => 'cust_pkg',
                             references => [ 'pkgnum' ],
                           },
+                          { columns    => [ 'commission_invnum' ],
+                            table      => 'cust_bill',
+                            references => [ 'invnum' ],
+                          },
                         ],
     },
 
@@ -1417,6 +1422,7 @@ sub tables_hashref {
         'commission_agentnum', 'int', 'NULL', '', '', '',
         'commission_salesnum', 'int', 'NULL', '', '', '',
         'commission_pkgnum',   'int', 'NULL', '', '', '',
+        'commission_invnum',   'int', 'NULL', '', '', '',
         #void fields
         'void_date',  @date_type,                  '', '', 
         'void_reason', 'varchar', 'NULL', $char_d, '', '', 
@@ -1456,6 +1462,10 @@ sub tables_hashref {
                             table      => 'cust_pkg',
                             references => [ 'pkgnum' ],
                           },
+                          { columns    => [ 'commission_invnum' ],
+                            table      => 'cust_bill',
+                            references => [ 'invnum' ],
+                          },
                           { columns    => [ 'void_reasonnum' ],
                             table      => 'reason',
                             references => [ 'reasonnum' ],
@@ -7438,6 +7448,36 @@ sub tables_hashref {
                         ],
     },
 
+    'commission_schedule' => {
+      'columns' => [
+        'schedulenum',    'serial',     '',      '', '', '',
+        'schedulename',  'varchar',     '', $char_d, '', '',
+        'reasonnum',         'int', 'NULL',      '', '', '',
+        'basis',         'varchar', 'NULL',      32, '', '',
+      ],
+      'primary_key'  => 'schedulenum',
+      'unique'       => [],
+      'index'        => [],
+    },
+
+    'commission_rate' => {
+      'columns' => [
+        'commissionratenum', 'serial',     '',      '', '', '',
+        'schedulenum',       'int',     '',      '', '', '',
+        'cycle',             'int',     '',      '', '', '',
+        'amount',            @money_type,          '', '', 
+        'percent',           'decimal','',   '7,4', '', '',
+      ],
+      'primary_key'  => 'commissionratenum',
+      'unique'       => [ [ 'schedulenum', 'cycle', ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns => [ 'schedulenum' ],
+                            table   => 'commission_schedule',
+                          },
+                        ],
+    },
     # name type nullability length default local
 
     #'new_table' => {
diff --git a/FS/FS/commission_rate.pm b/FS/FS/commission_rate.pm
new file mode 100644 (file)
index 0000000..dcb596d
--- /dev/null
@@ -0,0 +1,116 @@
+package FS::commission_rate;
+use base qw( FS::Record );
+
+use strict;
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::commission_rate - Object methods for commission_rate records
+
+=head1 SYNOPSIS
+
+  use FS::commission_rate;
+
+  $record = new FS::commission_rate \%hash;
+  $record = new FS::commission_rate { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::commission_rate object represents a commission rate (a percentage or a
+flat amount) that will be paid on a customer's N-th invoice. The sequence of
+commissions that will be paid on consecutive invoices is the parent object,
+L<FS::commission_schedule>.
+
+FS::commission_rate inherits from FS::Record.  The following fields are
+currently supported:
+
+=over 4
+
+=item commissionratenum - primary key
+
+=item schedulenum - L<FS::commission_schedule> foreign key
+
+=item cycle - the ordinal of the billing cycle this commission will apply
+to. cycle = 1 applies to the customer's first invoice, cycle = 2 to the
+second, etc.
+
+=item amount - the flat amount to pay per invoice in commission
+
+=item percent - the percentage of the invoice amount to pay in 
+commission
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new commission rate.  To add it to the database, see L<"insert">.
+
+=cut
+
+sub table { 'commission_rate'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=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.
+
+=item check
+
+Checks all fields to make sure this is a valid commission rate.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  $self->set('amount', '0.00')
+    if $self->get('amount') eq '';
+  $self->set('percent', '0')
+    if $self->get('percent') eq '';
+
+  my $error = 
+    $self->ut_numbern('commissionratenum')
+    || $self->ut_number('schedulenum')
+    || $self->ut_number('cycle')
+    || $self->ut_money('amount')
+    || $self->ut_decimal('percent')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/commission_schedule.pm b/FS/FS/commission_schedule.pm
new file mode 100644 (file)
index 0000000..375386c
--- /dev/null
@@ -0,0 +1,235 @@
+package FS::commission_schedule;
+use base qw( FS::o2m_Common FS::Record );
+
+use strict;
+use FS::Record qw( qsearch qsearchs );
+use FS::commission_rate;
+use Tie::IxHash;
+
+tie our %basis_options, 'Tie::IxHash', (
+  setuprecur    => 'Total sales',
+  setup         => 'One-time and setup charges',
+  recur         => 'Recurring charges',
+  setup_cost    => 'Setup costs',
+  recur_cost    => 'Recurring costs',
+  setup_margin  => 'Setup charges minus costs',
+  recur_margin_permonth => 'Monthly recurring charges minus costs',
+);
+
+=head1 NAME
+
+FS::commission_schedule - Object methods for commission_schedule records
+
+=head1 SYNOPSIS
+
+  use FS::commission_schedule;
+
+  $record = new FS::commission_schedule \%hash;
+  $record = new FS::commission_schedule { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::commission_schedule object represents a bundle of one or more
+commission rates for invoices. FS::commission_schedule inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item schedulenum - primary key
+
+=item schedulename - descriptive name
+
+=item reasonnum - the credit reason (L<FS::reason>) that will be assigned
+to these commission credits
+
+=item basis - for percentage credits, which component of the invoice charges
+the percentage will be calculated on:
+- setuprecur (total charges)
+- setup
+- recur
+- setup_cost
+- recur_cost
+- setup_margin (setup - setup_cost)
+- recur_margin_permonth ((recur - recur_cost) / freq)
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new commission schedule.  To add the object to the database, see
+L<"insert">.
+
+=cut
+
+sub table { 'commission_schedule'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+sub delete {
+  my $self = shift;
+  # don't allow the schedule to be removed if it's still linked to events
+  if ($self->part_event) {
+    return 'This schedule is still in use.'; # UI should be smarter
+  }
+  $self->process_o2m(
+    'table'   => 'commission_rate',
+    'params'  => [],
+  ) || $self->delete;
+}
+
+=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.
+
+=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
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('schedulenum')
+    || $self->ut_text('schedulename')
+    || $self->ut_number('reasonnum')
+    || $self->ut_enum('basis', [ keys %basis_options ])
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=item part_event
+
+Returns a list of billing events (L<FS::part_event> objects) that pay
+commission on this schedule.
+
+=cut
+
+sub part_event {
+  my $self = shift;
+  map { $_->part_event }
+    qsearch('part_event_option', {
+      optionname  => 'schedulenum',
+      optionvalue => $self->schedulenum,
+    }
+  );
+}
+
+=item calc_credit INVOICE
+
+Takes an L<FS::cust_bill> object and calculates credit on this schedule.
+Returns the amount to credit. If there's no rate defined for this invoice,
+returns nothing.
+
+=cut
+
+# Some false laziness w/ FS::part_event::Action::Mixin::credit_bill.
+# this is a little different in that we calculate the credit on the whole
+# invoice.
+
+sub calc_credit {
+  my $self = shift;
+  my $cust_bill = shift;
+  die "cust_bill record required" if !$cust_bill or !$cust_bill->custnum;
+  # count invoices before or including this one
+  my $cycle = FS::cust_bill->count('custnum = ? AND _date <= ?',
+    $cust_bill->custnum,
+    $cust_bill->_date
+  );
+  my $rate = qsearchs('commission_rate', {
+    schedulenum => $self->schedulenum,
+    cycle       => $cycle,
+  });
+  # we might do something with a rate that applies "after the end of the
+  # schedule" (cycle = 0 or something) so that this can do commissions with
+  # no end date. add that here if there's a need.
+  return unless $rate;
+
+  my $amount;
+  if ( $rate->percent ) {
+    my $what = $self->basis;
+    my $cost = ($what =~ /_cost/ ? 1 : 0);
+    my $margin = ($what =~ /_margin/ ? 1 : 0);
+    my %part_pkg_cache;
+    foreach my $cust_bill_pkg ( $cust_bill->cust_bill_pkg ) {
+
+      my $charge = 0;
+      next if !$cust_bill_pkg->pkgnum; # exclude taxes and fees
+
+      my $cust_pkg = $cust_bill_pkg->cust_pkg;
+      if ( $margin or $cost ) {
+        # look up package costs only if we need them
+        my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
+        my $part_pkg   = $part_pkg_cache{$pkgpart}
+                     ||= FS::part_pkg->by_key($pkgpart);
+
+        if ( $cost ) {
+          $charge = $part_pkg->get($what);
+        } else { # $margin
+          $charge = $part_pkg->$what($cust_pkg);
+        }
+
+        $charge = ($charge || 0) * ($cust_pkg->quantity || 1);
+
+      } else {
+
+        if ( $what eq 'setup' ) {
+          $charge = $cust_bill_pkg->get('setup');
+        } elsif ( $what eq 'recur' ) {
+          $charge = $cust_bill_pkg->get('recur');
+        } elsif ( $what eq 'setuprecur' ) {
+          $charge = $cust_bill_pkg->get('setup') +
+                    $cust_bill_pkg->get('recur');
+        }
+      }
+
+      $amount += ($charge * $rate->percent / 100);
+
+    }
+  } # if $rate->percent
+
+  if ( $rate->amount ) {
+    $amount += $rate->amount;
+  }
+
+  $amount = sprintf('%.2f', $amount + 0.005);
+  return $amount;
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::part_event>, L<FS::commission_rate>
+
+=cut
+
+1;
+
index 8546372..e4b1fc0 100644 (file)
@@ -315,6 +315,7 @@ sub check {
     || $self->ut_foreign_keyn('commission_agentnum',  'agent', 'agentnum')
     || $self->ut_foreign_keyn('commission_salesnum',  'sales', 'salesnum')
     || $self->ut_foreign_keyn('commission_pkgnum', 'cust_pkg', 'pkgnum')
+    || $self->ut_foreign_keyn('commission_invnum', 'cust_bill', 'invnum')
   ;
   return $error if $error;
 
diff --git a/FS/FS/part_event/Action/bill_agent_credit_schedule.pm b/FS/FS/part_event/Action/bill_agent_credit_schedule.pm
new file mode 100644 (file)
index 0000000..31189a2
--- /dev/null
@@ -0,0 +1,76 @@
+package FS::part_event::Action::bill_agent_credit_schedule;
+
+use base qw( FS::part_event::Action );
+use FS::Conf;
+use FS::cust_credit;
+use FS::commission_schedule;
+use Date::Format qw(time2str);
+
+use strict;
+
+sub description { 'Credit the agent based on a commission schedule' }
+
+sub option_fields {
+  'schedulenum' => { 'label'        => 'Schedule',
+                     'type'         => 'select-table',
+                     'table'        => 'commission_schedule',
+                     'name_col'     => 'schedulename',
+                     'disable_empty'=> 1,
+                   },
+}
+
+sub eventtable_hashref {
+  { 'cust_bill' => 1 };
+}
+
+our $date_format;
+
+sub do_action {
+  my( $self, $cust_bill, $cust_event ) = @_;
+
+  $date_format ||= FS::Conf->new->config('date_format') || '%x';
+
+  my $cust_main = $self->cust_main($cust_bill);
+  my $agent = $cust_main->agent;
+  return "No customer record for agent ". $agent->agent
+    unless $agent->agent_custnum;
+
+  my $agent_cust_main = $agent->agent_cust_main;
+
+  my $schedulenum = $self->option('schedulenum')
+    or return "no commission schedule selected";
+  my $schedule = FS::commission_schedule->by_key($schedulenum)
+    or return "commission schedule #$schedulenum not found";
+    # commission_schedule::delete tries to prevent this, but just in case
+
+  my $amount = $schedule->calc_credit($cust_bill)
+    or return;
+
+  my $reasonnum = $schedule->reasonnum;
+
+  #XXX shouldn't do this here, it's a localization problem.
+  # credits with commission_invnum should know how to display it as part
+  # of invoice rendering.
+  my $desc = 'from invoice #'. $cust_bill->display_invnum .
+             ' ('. time2str($date_format, $cust_bill->_date) . ')';
+             # could also show custnum and pkgnums here?
+  my $cust_credit = FS::cust_credit->new({
+    'custnum'             => $agent_cust_main->custnum,
+    'reasonnum'           => $reasonnum,
+    'amount'              => $amount,
+    'eventnum'            => $cust_event->eventnum,
+    'addlinfo'            => $desc,
+    'commission_agentnum' => $cust_main->agentnum,
+    'commission_invnum'   => $cust_bill->invnum,
+  });
+  my $error = $cust_credit->insert;
+  die "Error crediting customer ". $agent_cust_main->custnum.
+      " for agent commission: $error"
+    if $error;
+
+  #return $warning; # currently don't get warnings here
+  return;
+
+}
+
+1;
index 83359f1..4184b9c 100644 (file)
@@ -870,3 +870,7 @@ FS/webservice_log.pm
 t/webservice_log.t
 FS/access_user_page_pref.pm
 t/access_user_page_pref.t
+FS/commission_schedule.pm
+t/commission_schedule.t
+FS/commission_rate.pm
+t/commission_rate.t
diff --git a/FS/t/commission_rate.t b/FS/t/commission_rate.t
new file mode 100644 (file)
index 0000000..fb5f43c
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::commission_rate;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/commission_schedule.t b/FS/t/commission_schedule.t
new file mode 100644 (file)
index 0000000..bbe6b42
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::commission_schedule;
+$loaded=1;
+print "ok 1\n";
diff --git a/httemplate/browse/commission_schedule.html b/httemplate/browse/commission_schedule.html
new file mode 100644 (file)
index 0000000..5a4f984
--- /dev/null
@@ -0,0 +1,70 @@
+<& elements/browse.html,
+  'title'       => "Commission schedules",
+  'name'        => "commission schedules",
+  'menubar'     => [ 'Add a new schedule' =>
+                        $p.'edit/commission_schedule.html'
+                   ],
+  'query'       => { 'table'     => 'commission_schedule', },
+  'count_query' => 'SELECT COUNT(*) FROM commission_schedule',
+  'header'      => [ '#',
+                     'Name',
+                     'Rates',
+                   ],
+  'fields'      => [ 'schedulenum',
+                     'schedulename',
+                     $rates_sub,
+                  ],
+  'links'       => [ $link,
+                     $link,
+                     '',
+                   ],
+  'disable_total' => 1,
+&>
+<%init>
+
+my $money_char = FS::Conf->new->config('money_char') || '$';
+
+my $ordinal_sub = sub {
+  # correct from 1 to 12...
+  my $num = shift;
+  $num == 1 ? '1st' :
+  $num == 2 ? '2nd' :
+  $num == 3 ? '3rd' :
+  $num . 'th'
+};
+
+my $rates_sub = sub {
+  my $schedule = shift;
+  my @rates = sort { $a->cycle <=> $b->cycle } $schedule->commission_rate;
+  my @data;
+  my $basis = emt(lc( $FS::commission_schedule::basis_options{$schedule->basis} ));
+  foreach my $rate (@rates) {
+    my $desc = '';
+    if ( $rate->amount > 0 ) {
+      $desc = $money_char . sprintf('%.2f', $rate->amount);
+    }
+    if ( $rate->percent > 0 ) {
+      $desc .= ' + ' if $desc;
+      $desc .= $rate->percent . '% ' . emt('of') . ' ' . $basis;
+    }
+    next if !$desc;
+    $desc = &$ordinal_sub($rate->cycle) . ' ' . emt('invoice') .
+             ':&nbsp;' . $desc;
+
+    push @data,
+    [
+      {
+        'data'  => $desc,
+        'align' => 'right',
+      }
+    ];
+  }
+  \@data;
+};
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $link = [ $p.'edit/commission_schedule.html?', 'schedulenum' ];
+
+</%init>
diff --git a/httemplate/edit/commission_schedule.html b/httemplate/edit/commission_schedule.html
new file mode 100644 (file)
index 0000000..c76a361
--- /dev/null
@@ -0,0 +1,53 @@
+<& elements/edit.html,
+     name_singular => 'schedule',
+     table         => 'commission_schedule',
+     viewall_dir   => 'browse',
+     fields        => [ 'schedulename',
+                        { field             => 'reasonnum',
+                          type              => 'select-reason',
+                          reason_class      => 'R',
+                        },
+                        { field             => 'basis',
+                          type              => 'select',
+                          options           => [ keys %FS::commission_schedule::basis_options ],
+                          labels            => { %FS::commission_schedule::basis_options },
+                        },
+                        { type => 'tablebreak-tr-title', value => 'Billing cycles' },
+                        { field             => 'commissionratenum',
+                          type              => 'commission_rate',
+                          o2m_table         => 'commission_rate',
+                          m2_label          => ' ',
+                          m2_error_callback => $m2_error_callback,
+                          colspan => 2,
+                        },
+                      ],
+     labels        => { 'schedulenum'       => '',
+                        'schedulename'      => 'Name',
+                        'basis'             => 'Based on',
+                        'commissionratenum' => '',
+                      },
+&>
+<%init>
+
+my $m2_error_callback = sub {
+  my ($cgi, $object) = @_;
+
+  my @rates;
+  foreach my $k ( grep /^commissionratenum\d+/, $cgi->param ) {
+    my $num = $cgi->param($k);
+    my $cycle = $cgi->param($k.'_cycle');
+    my $amount = $cgi->param($k.'_amount');
+    my $percent = $cgi->param($k.'_percent');
+    if ($cycle > 0) {
+      push @rates, FS::commission_rate->new({
+        'commissionratenum' => $num,
+        'cycle'             => $cycle,
+        'amount'            => $amount,
+        'percent'           => $percent,
+      });
+    }
+  }
+  @rates;
+};
+
+</%init>
diff --git a/httemplate/edit/process/commission_schedule.html b/httemplate/edit/process/commission_schedule.html
new file mode 100644 (file)
index 0000000..50e0371
--- /dev/null
@@ -0,0 +1,36 @@
+<& elements/process.html,
+  'table'       => 'commission_schedule',
+  'viewall_dir' => 'browse',
+  'process_o2m' => {
+   'table'  => 'commission_rate',
+   'fields' => [qw( cycle amount percent )],
+  },
+  'precheck_callback' => $precheck,
+  'debug' => 1,
+&>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $precheck = sub {
+  my $cgi = shift;
+  $cgi->param('reasonnum') =~ /^(-?\d+)$/ or die "Illegal reasonnum";
+
+  my ($reasonnum, $error) = $m->comp('/misc/process/elements/reason');
+  if (!$reasonnum) {
+    $error ||= 'Reason required'
+  }
+  $cgi->param('reasonnum', $reasonnum) unless $error;
+
+  # remove rate entries with no cycle selected
+  foreach my $k (grep /^commissionratenum\d+$/, $cgi->param) {
+    if (! $cgi->param($k.'_cycle') ) {
+      $cgi->delete($k);
+    }
+  }
+
+  $error;
+};
+
+</%init>
diff --git a/httemplate/elements/commission_rate.html b/httemplate/elements/commission_rate.html
new file mode 100644 (file)
index 0000000..071ebb1
--- /dev/null
@@ -0,0 +1,68 @@
+% unless ( $opt{'js_only'} ) {
+
+  <INPUT TYPE="hidden" NAME="<%$name%>" ID="<%$id%>" VALUE="<% $curr_value %>">
+
+      <& select.html,
+        field         => "${name}_cycle",
+        options       => [ '', 1 .. 12 ],
+        option_labels => {
+          ''  => '',
+          1   => '1st',
+          2   => '2nd',
+          3   => '3rd',
+          map { $_ => $_.'th' } 4 .. 12
+        },
+        onchange      => $onchange,
+        curr_value    => $commission_rate->get("cycle"),
+      &>
+      <B><% $money_char %></B>
+      <& input-text.html,
+        field         => "${name}_amount",
+        size          => 8,
+        curr_value    => $commission_rate->get("amount")
+                         || '0.00',
+        'text-align'  => 'right'
+      &>
+      <B> + </B>
+      <& input-text.html,
+        field         => "${name}_percent",
+        size          => 8,
+        curr_value    => $commission_rate->get("percent")
+                         || '0',
+        'text-align'  => 'right'
+      &><B>%</B>
+% }
+<%init>
+
+my( %opt ) = @_;
+
+my $conf = new FS::Conf;
+my $money_char = $conf->config('money_char') || '$';
+
+my $name = $opt{'field'} || 'commissionratenum';
+my $id = $opt{'id'} || 'commissionratenum';
+
+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 $commission_rate;
+if ( $curr_value ) {
+  $commission_rate = qsearchs('commission_rate', { 'commissionratenum' => $curr_value } );
+} else {
+  $commission_rate = new FS::commission_rate {};
+}
+
+foreach my $field (qw( amount percent cycle)) {
+  my $value = $cgi->param("${name}_${field}");
+  $commission_rate->set($field, $value) if $value;
+}
+
+</%init>
index 0f98bc9..88c1df3 100644 (file)
@@ -672,7 +672,10 @@ $config_cust{'Note classes'} = [ $fsurl.'browse/cust_note_class.html', 'Note cla
 tie my %config_agent, 'Tie::IxHash',
   'Agent types' => [ $fsurl.'browse/agent_type.cgi', 'Agent types define groups of package definitions that you can then assign to particular agents' ],
   'Agents'      => [ $fsurl.'browse/agent.cgi', 'Agents are resellers of your service. Agents may be limited to a subset of your full offerings (via their type)' ],
-  'Agent payment gateways'         => [ $fsurl.'browse/payment_gateway.html', 'Credit card and electronic check processors for agent overrides' ];
+  'Agent payment gateways'         => [ $fsurl.'browse/payment_gateway.html', 'Credit card and electronic check processors for agent overrides' ],
+  'separator' => '',
+  'Commission schedules' => [ $fsurl.'browse/commission_schedule.html',
+    'Commission schedules for consecutive billing periods' ],
 ;
 
 tie my %config_sales, 'Tie::IxHash',
index 97466f1..9a43022 100755 (executable)
@@ -188,9 +188,8 @@ my $class = $opt{'reason_class'};
 my $init_reason;
 if ( $opt{'cgi'} ) {
   $init_reason = $opt{'cgi'}->param($name);
-} else {
-  $init_reason = $opt{'curr_value'};
 }
+$init_reason ||= $opt{'curr_value'};
 
 my $id = $opt{'id'} || $name;
 $id =~ s/\./_/g; # for edit/part_event