[freeside-commits] branch master updated. 6422e165313ee8d67790007581821217240734fb

Mark Wells mark at 420.am
Sat Oct 12 18:50:43 PDT 2013


The branch, master has been updated
       via  6422e165313ee8d67790007581821217240734fb (commit)
       via  cd365522ec4e9f1b553bb1b5096c756c1fdb1d01 (commit)
       via  65f3ee811a83be47ef02e0919ec122ad4083ccbd (commit)
      from  276028234bd1c9f8ad736d9be2062803040700f7 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 6422e165313ee8d67790007581821217240734fb
Author: Mark Wells <mark at freeside.biz>
Date:   Sat Oct 12 18:44:52 2013 -0700

    allow changing package class of one-time charges post-billing, #25342

diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm
index 2783ada..ca96eb5 100644
--- a/FS/FS/AccessRight.pm
+++ b/FS/FS/AccessRight.pm
@@ -130,6 +130,7 @@ tie my %rights, 'Tie::IxHash',
     'View customer packages', #NEW
     'Order customer package',
     'One-time charge',
+    'Modify one-time charge',
     'Change customer package',
     'Detach customer package',
     'Bulk change customer packages',
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 1215ca4..398d785 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -356,6 +356,7 @@ if ( -e $addl_handler_use_file ) {
   use FS::invoice_mode;
   use FS::invoice_conf;
   use FS::cable_provider;
+  use FS::cust_credit_void;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index b6f3cf3..3029ab5 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -1018,6 +1018,37 @@ sub tables_hashref {
                  ],
     },
 
+    'cust_credit_void' => {
+      'columns' => [
+        'crednum',  'serial',     '', '', '', '', 
+        'custnum',     'int',     '', '', '', '', 
+        '_date',  @date_type,             '', '', 
+        'amount', at money_type,             '', '', 
+        'currency',   'char', 'NULL',  3, '', '',
+        'otaker',  'varchar', 'NULL', 32, '', '', 
+        'usernum',     'int', 'NULL', '', '', '',
+        'reason',     'text', 'NULL', '', '', '', 
+        'reasonnum',   'int', 'NULL', '', '', '', 
+        'addlinfo',   'text', 'NULL', '', '', '',
+        'closed',     'char', 'NULL',  1, '', '', 
+        'pkgnum',      'int', 'NULL', '', '','',
+        'eventnum',    'int', 'NULL', '', '','',
+        'commission_agentnum', 'int', 'NULL', '', '', '',
+        'commission_salesnum', 'int', 'NULL', '', '', '',
+        'commission_pkgnum',   'int', 'NULL', '', '', '',
+        #void fields
+        'void_date',  @date_type,                  '', '', 
+        'void_reason', 'varchar', 'NULL', $char_d, '', '', 
+        'void_usernum',    'int', 'NULL',      '', '', '',
+      ],
+      'primary_key' => 'crednum',
+      'unique' => [],
+      'index' => [ ['custnum'], ['_date'], ['usernum'], ['eventnum'],
+                   [ 'commission_salesnum' ],
+                 ],
+    },
+
+
     'cust_credit_bill' => {
       'columns' => [
         'creditbillnum', 'serial', '', '', '', '', 
diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm
index bd92bdc..9678934 100644
--- a/FS/FS/cust_credit.pm
+++ b/FS/FS/cust_credit.pm
@@ -21,6 +21,7 @@ use FS::reason;
 use FS::cust_event;
 use FS::agent;
 use FS::sales;
+use FS::cust_credit_void;
 
 $me = '[ FS::cust_credit ]';
 $DEBUG = 0;
@@ -203,6 +204,8 @@ the void method instead to leave a record of the deleted credit.
 # very similar to FS::cust_pay::delete
 sub delete {
   my $self = shift;
+  my %opt = @_;
+
   return "Can't delete closed credit" if $self->closed =~ /^Y/i;
 
   local $SIG{HUP} = 'IGNORE';
@@ -238,7 +241,7 @@ sub delete {
     return $error;
   }
 
-  if ( $conf->config('deletecredits') ne '' ) {
+  if ( !$opt{void} and $conf->config('deletecredits') ne '' ) {
 
     my $cust_main = $self->cust_main;
 
@@ -336,6 +339,53 @@ sub check {
   $self->SUPER::check;
 }
 
+=item void [ REASON ]
+
+Voids this credit: deletes the credit and all associated applications and 
+adds a record of the voided credit to the cust_credit_void table.
+
+=cut
+
+# yes, false laziness with cust_pay and cust_bill
+# but frankly I don't have time to fix it now
+
+sub void {
+  my $self = shift;
+  my $reason = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $cust_credit_void = new FS::cust_credit_void ( {
+      map { $_ => $self->get($_) } $self->fields
+    } );
+  $cust_credit_void->set('void_reason', $reason);
+  my $error = $cust_credit_void->insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $error = $self->delete(void => 1); # suppress deletecredits warning
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
 =item cust_credit_refund
 
 Returns all refund applications (see L<FS::cust_credit_refund>) for this credit.
diff --git a/FS/FS/cust_credit_void.pm b/FS/FS/cust_credit_void.pm
new file mode 100644
index 0000000..ac47d95
--- /dev/null
+++ b/FS/FS/cust_credit_void.pm
@@ -0,0 +1,134 @@
+package FS::cust_credit_void; 
+
+use strict;
+use base qw( FS::otaker_Mixin FS::cust_main_Mixin FS::Record );
+use FS::Record qw(qsearch qsearchs dbh fields);
+use FS::CurrentUser;
+use FS::access_user;
+use FS::cust_credit;
+
+=head1 NAME
+
+FS::cust_credit_void - Object methods for cust_credit_void objects
+
+=head1 SYNOPSIS
+
+  use FS::cust_credit_void;
+
+  $record = new FS::cust_credit_void \%hash;
+  $record = new FS::cust_credit_void { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_credit_void object represents a voided credit.  All fields in
+FS::cust_credit are present, as well as:
+
+=over 4
+
+=item void_date - the date (unix timestamp) that the credit was voided
+
+=item void_reason - the reason (a freeform string)
+
+=item void_usernum - the user (L<FS::access_user>) who voided it
+
+=back
+
+=head1 METHODS
+
+=over 4 
+
+=item new HASHREF
+
+Creates a new voided credit record.
+
+=cut
+
+sub table { 'cust_credit_void'; }
+
+=item insert
+
+Adds this voided credit to the database.
+
+=item check
+
+Checks all fields to make sure this is a valid voided credit.  If there is an
+error, returns the error, otherwise returns false.  Called by the insert
+method.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error =
+    $self->ut_numbern('crednum')
+    || $self->ut_number('custnum')
+    || $self->ut_numbern('_date')
+    || $self->ut_money('amount')
+    || $self->ut_alphan('otaker')
+    || $self->ut_textn('reason')
+    || $self->ut_textn('addlinfo')
+    || $self->ut_enum('closed', [ '', 'Y' ])
+    || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
+    || $self->ut_foreign_keyn('eventnum', 'cust_event', 'eventnum')
+    || $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_numbern('void_date')
+    || $self->ut_textn('void_reason')
+    || $self->ut_foreign_keyn('void_usernum', 'access_user', 'usernum')
+  ;
+  return $error if $error;
+
+  $self->void_date(time) unless $self->void_date;
+
+  $self->void_usernum($FS::CurrentUser::CurrentUser->usernum)
+    unless $self->void_usernum;
+
+  $self->SUPER::check;
+}
+
+=item cust_main
+
+Returns the parent customer object (see L<FS::cust_main>).
+
+=cut
+
+sub cust_main {
+  my $self = shift;
+  qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+}
+
+=item void_access_user
+
+Returns the voiding employee object (see L<FS::access_user>).
+
+=cut
+
+sub void_access_user {
+  my $self = shift;
+  qsearchs('access_user', { 'usernum' => $self->void_usernum } );
+}
+
+=back
+
+=head1 BUGS
+
+Doesn't yet support unvoid.
+
+=head1 SEE ALSO
+
+L<FS::cust_credit>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index a9a4cb0..3e36c60 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -1243,13 +1243,14 @@ sub merge {
   }
 
   tie my %financial_tables, 'Tie::IxHash',
-    'cust_bill'      => 'invoices',
-    'cust_bill_void' => 'voided invoices',
-    'cust_statement' => 'statements',
-    'cust_credit'    => 'credits',
-    'cust_pay'       => 'payments',
-    'cust_pay_void'  => 'voided payments',
-    'cust_refund'    => 'refunds',
+    'cust_bill'         => 'invoices',
+    'cust_bill_void'    => 'voided invoices',
+    'cust_statement'    => 'statements',
+    'cust_credit'       => 'credits',
+    'cust_credit_void'  => 'voided credits',
+    'cust_pay'          => 'payments',
+    'cust_pay_void'     => 'voided payments',
+    'cust_refund'       => 'refunds',
   ;
    
   foreach my $table ( keys %financial_tables ) {
@@ -3732,6 +3733,19 @@ sub cust_credit_pkgnum {
     );
 }
 
+=item cust_credit_void
+
+Returns all voided credits (see L<FS::cust_credit_void>) for this customer.
+
+=cut
+
+sub cust_credit_void {
+  my $self = shift;
+  map { $_ }
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_credit_void', { 'custnum' => $self->custnum } )
+}
+
 =item cust_pay
 
 Returns all the payments (see L<FS::cust_pay>) for this customer.
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index 066b987..be5ec6a 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -35,6 +35,8 @@ use FS::cust_pkg_discount;
 use FS::discount;
 use FS::UI::Web;
 use FS::sales;
+# for modify_charge
+use FS::cust_credit;
 
 # need to 'use' these instead of 'require' in sub { cancel, suspend, unsuspend,
 # setup }
@@ -2256,8 +2258,128 @@ sub set_salesnum {
   $self = $self->replace_old; # just to make sure
   $self->salesnum(shift);
   $self->replace;
+  # XXX this should probably reassign any credit that's already been given
 }
 
+=item modify_charge OPTIONS
+
+Change the properties of a one-time charge.  Currently the only properties
+that can be changed this way are those that have no impact on billing 
+calculations:
+- pkg: the package description
+- classnum: the package class
+- additional: arrayref of additional invoice details to add to this package
+
+If you pass 'adjust_commission' => 1, and the classnum changes, and there are
+commission credits linked to this charge, they will be recalculated.
+
+=cut
+
+sub modify_charge {
+  my $self = shift;
+  my %opt = @_;
+  my $part_pkg = $self->part_pkg;
+  my $pkgnum = $self->pkgnum;
+
+  my $dbh = dbh;
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+
+  return "Can't use modify_charge except on one-time charges"
+    unless $part_pkg->freq eq '0';
+
+  if ( length($opt{'pkg'}) and $part_pkg->pkg ne $opt{'pkg'} ) {
+    $part_pkg->set('pkg', $opt{'pkg'});
+  }
+
+  my %pkg_opt = $part_pkg->options;
+  if ( ref($opt{'additional'}) ) {
+    delete $pkg_opt{$_} foreach grep /^additional/, keys %pkg_opt;
+    my $i;
+    for ( $i = 0; exists($opt{'additional'}->[$i]); $i++ ) {
+      $pkg_opt{ "additional_info$i" } = $opt{'additional'}->[$i];
+    }
+    $pkg_opt{'additional_count'} = $i if $i > 0;
+  }
+
+  my $old_classnum;
+  if ( exists($opt{'classnum'}) and $part_pkg->classnum ne $opt{'classnum'} ) {
+    # remember it
+    $old_classnum = $part_pkg->classnum;
+    $part_pkg->set('classnum', $opt{'classnum'});
+  }
+
+  my $error = $part_pkg->replace( options => \%pkg_opt );
+  return $error if $error;
+
+  if (defined $old_classnum) {
+    # fix invoice grouping records
+    my $old_catname = $old_classnum
+                      ? FS::pkg_class->by_key($old_classnum)->categoryname
+                      : '';
+    my $new_catname = $opt{'classnum'}
+                      ? $part_pkg->pkg_class->categoryname
+                      : '';
+    if ( $old_catname ne $new_catname ) {
+      foreach my $cust_bill_pkg ($self->cust_bill_pkg) {
+        # (there should only be one...)
+        my @display = qsearch( 'cust_bill_pkg_display', {
+            'billpkgnum'  => $cust_bill_pkg->billpkgnum,
+            'section'     => $old_catname,
+        });
+        foreach (@display) {
+          $_->set('section', $new_catname);
+          $error = $_->replace;
+          if ( $error ) {
+            $dbh->rollback if $oldAutoCommit;
+            return $error;
+          }
+        }
+      } # foreach $cust_bill_pkg
+    }
+
+    if ( $opt{'adjust_commission'} ) {
+      # fix commission credits...tricky.
+      foreach my $cust_event ($self->cust_event) {
+        my $part_event = $cust_event->part_event;
+        foreach my $table (qw(sales agent)) {
+          my $class =
+            "FS::part_event::Action::Mixin::credit_${table}_pkg_class";
+          my $credit = qsearchs('cust_credit', {
+              'eventnum' => $cust_event->eventnum,
+          });
+          if ( $part_event->isa($class) ) {
+            # Yes, this results in current commission rates being applied 
+            # retroactively to a one-time charge.  For accounting purposes 
+            # there ought to be some kind of time limit on doing this.
+            my $amount = $part_event->_calc_credit($self);
+            if ( $credit and $credit->amount ne $amount ) {
+              # Void the old credit.
+              $error = $credit->void('Package class changed');
+              if ( $error ) {
+                $dbh->rollback if $oldAutoCommit;
+                return "$error (adjusting commission credit)";
+              }
+            }
+            # redo the event action to recreate the credit.
+            local $@ = '';
+            eval { $part_event->do_action( $self, $cust_event ) };
+            if ( $@ ) {
+              $dbh->rollback if $oldAutoCommit;
+              return $@;
+            }
+          } # if $part_event->isa($class)
+        } # foreach $table
+      } # foreach $cust_event
+    } # if $opt{'adjust_commission'}
+  } # if defined $old_classnum
+
+  $dbh->commit if $oldAutoCommit;
+  '';
+}
+
+
+
 use Storable 'thaw';
 use MIME::Base64;
 use Data::Dumper;
diff --git a/FS/MANIFEST b/FS/MANIFEST
index 5dbe754..7a460da 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -726,3 +726,5 @@ FS/invoice_conf.pm
 t/invoice_conf.t
 FS/cable_provider.pm
 t/cable_provider.t
+FS/cust_credit_void.pm
+t/cust_credit_void.t
diff --git a/FS/t/cust_credit_void.t b/FS/t/cust_credit_void.t
new file mode 100644
index 0000000..6113ef5
--- /dev/null
+++ b/FS/t/cust_credit_void.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_credit_void;
+$loaded=1;
+print "ok 1\n";
diff --git a/httemplate/edit/process/quick-charge.cgi b/httemplate/edit/process/quick-charge.cgi
index 38f06e1..db41fb2 100644
--- a/httemplate/edit/process/quick-charge.cgi
+++ b/httemplate/edit/process/quick-charge.cgi
@@ -10,8 +10,9 @@
 % }
 <%init>
 
+my $curuser = $FS::CurrentUser::CurrentUser;
 die "access denied"
-  unless $FS::CurrentUser::CurrentUser->access_right('One-time charge');
+  unless $curuser->access_right('One-time charge');
 
 my $error = '';
 my $conf = new FS::conf;
@@ -27,49 +28,76 @@ $param->{"custnum"} =~ /^(\d+)$/
   or $error .= "Illegal customer number " . $param->{"custnum"} . "  ";
 my $custnum = $1;
 
-$param->{"amount"} =~ /^\s*(\d*(?:\.?\d{1,2}))\s*$/
-  or $error .= "Illegal amount " . $param->{"amount"} . "  ";
-my $amount = $1;
+my $cust_main = FS::cust_main->by_key($custnum)
+  or die "custnum $custnum not found";
 
-my $quantity = 1;
-if ( $cgi->param('quantity') =~ /^\s*(\d+)\s*$/ ) {
-  $quantity = $1;
-}
+exists($curuser->agentnums_href->{$cust_main->agentnum})
+  or die "access denied";
 
-$param->{'tax_override'} =~ /^\s*([,\d]*)\s*$/
-  or $error .= "Illegal tax override " . $param->{"tax_override"} . "  ";
-my $override = $1;
+if ( $param->{'pkgnum'} =~ /^(\d+)$/ ) {
+  my $pkgnum = $1;
+  die "access denied"
+    unless $curuser->access_right('Modify one-time charge');
 
-if ( $param->{'taxclass'} eq '(select)' ) {
-  $error .= "Must select a tax class.  "
-    unless ($conf->exists('enable_taxproducts') &&
-             ( $override || $param->{taxproductnum} )
-           );
-  $cgi->param('taxclass', '');
-}
+  my $cust_pkg = FS::cust_pkg->by_key($1)
+    or die "pkgnum $pkgnum not found";
+
+  my $part_pkg = $cust_pkg->part_pkg;
+  die "pkgnum $pkgnum is not a one-time charge" unless $part_pkg->freq eq '0';
+
+  $error = $cust_pkg->modify_charge(
+      'pkg'               => scalar($cgi->param('pkg')),
+      'classnum'          => scalar($cgi->param('classnum')),
+      'additional'        => \@description,
+      'adjust_commission' => ($cgi->param('adjust_commission') ? 1 : 0),
+  );
+
+} else {
+  # the usual case: new one-time charge
+  $param->{"amount"} =~ /^\s*(\d*(?:\.?\d{1,2}))\s*$/
+    or $error .= "Illegal amount " . $param->{"amount"} . "  ";
+  my $amount = $1;
+
+  my $quantity = 1;
+  if ( $cgi->param('quantity') =~ /^\s*(\d+)\s*$/ ) {
+    $quantity = $1;
+  }
+
+  $param->{'tax_override'} =~ /^\s*([,\d]*)\s*$/
+    or $error .= "Illegal tax override " . $param->{"tax_override"} . "  ";
+  my $override = $1;
+
+  if ( $param->{'taxclass'} eq '(select)' ) {
+    $error .= "Must select a tax class.  "
+      unless ($conf->exists('enable_taxproducts') &&
+               ( $override || $param->{taxproductnum} )
+             );
+    $cgi->param('taxclass', '');
+  }
+
+  unless ( $error ) {
+    my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+      or $error .= "Unknown customer number $custnum.  ";
 
-unless ( $error ) {
-  my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
-    or $error .= "Unknown customer number $custnum.  ";
-
-  $error ||= $cust_main->charge( {
-    'amount'        => $amount,
-    'quantity'      => $quantity,
-    'bill_now'      => scalar($cgi->param('bill_now')),
-    'invoice_terms' => scalar($cgi->param('invoice_terms')),
-    'start_date'    => ( scalar($cgi->param('start_date'))
-                           ? parse_datetime($cgi->param('start_date'))
-                           : ''
-                       ),
-    'no_auto'       => scalar($cgi->param('no_auto')),
-    'pkg'           => scalar($cgi->param('pkg')),
-    'setuptax'      => scalar($cgi->param('setuptax')),
-    'taxclass'      => scalar($cgi->param('taxclass')),
-    'taxproductnum' => scalar($cgi->param('taxproductnum')),
-    'tax_override'  => $override,
-    'classnum'      => scalar($cgi->param('classnum')),
-    'additional'    => \@description,
-  } );
+    $error ||= $cust_main->charge( {
+      'amount'        => $amount,
+      'quantity'      => $quantity,
+      'bill_now'      => scalar($cgi->param('bill_now')),
+      'invoice_terms' => scalar($cgi->param('invoice_terms')),
+      'start_date'    => ( scalar($cgi->param('start_date'))
+                             ? parse_datetime($cgi->param('start_date'))
+                             : ''
+                         ),
+      'no_auto'       => scalar($cgi->param('no_auto')),
+      'pkg'           => scalar($cgi->param('pkg')),
+      'setuptax'      => scalar($cgi->param('setuptax')),
+      'taxclass'      => scalar($cgi->param('taxclass')),
+      'taxproductnum' => scalar($cgi->param('taxproductnum')),
+      'tax_override'  => $override,
+      'classnum'      => scalar($cgi->param('classnum')),
+      'additional'    => \@description,
+    } );
+  }
 }
 
 </%init>
diff --git a/httemplate/edit/quick-charge.html b/httemplate/edit/quick-charge.html
index 466091d..666ba82 100644
--- a/httemplate/edit/quick-charge.html
+++ b/httemplate/edit/quick-charge.html
@@ -104,6 +104,49 @@ function bill_now_changed (what) {
 
 <TABLE ID="QuickChargeTable" BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0 STYLE="background-color: #cccccc">
 
+% if ( $cust_pkg ) {
+
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $cust_pkg->pkgnum %>">
+<& /elements/tr-fixed.html,
+  label => 'Amount',
+  field => 'amount',
+  value => $money_char . sprintf('%.2f',$part_pkg->option('setup_fee')),
+&>
+
+%   if ( $conf->exists('invoice-unitprice') ) {
+<& /elements/tr-fixed.html,
+  label => 'Quantity',
+  field => 'quantity',
+  value => $cust_pkg->quantity
+&>
+%   }
+
+<& /elements/tr-select-pkg_class.html, 'curr_value' => $classnum  &>
+
+% # crudely estimate whether any agent commission credits might exist
+%   my @events = grep { $_->part_event->action =~ /credit/ }
+%                $cust_pkg->cust_event;
+%   if ( scalar @events ) {
+<TR><TD></TD>
+  <TD><INPUT TYPE="checkbox" NAME="adjust_commission" VALUE="Y" CHECKED>
+<% emt('Adjust commission credits if necessary') %>
+</TD>
+</TR>
+%   }
+
+% #display the future or past charge date, but don't allow changes
+% # XXX we probably _could_ let as-yet unbilled charges be rescheduled, but
+% # there's no compelling need yet
+%   if ( $cust_pkg->setup or $cust_pkg->start_date ) {
+%     my $label = $cust_pkg->setup ? emt('Billed on') : emt('Will be billed');
+%     my $field = $cust_pkg->setup ? 'setup' : 'start_date';
+      <& /elements/tr-fixed-date.html,
+        label => $label,
+        value => $cust_pkg->get($field)
+      &>
+%   } # else we don't show anything here
+% } else { # new one-time charge
+
 <TR>
   <TD ALIGN="right"><% mt('Amount') |h %> </TD>
   <TD>
@@ -117,7 +160,7 @@ function bill_now_changed (what) {
   </TD>
 </TR>
 
-% if ( $conf->exists('invoice-unitprice') ) {
+%   if ( $conf->exists('invoice-unitprice') ) {
     <TR>
       <TD ALIGN="right"><% mt('Quantity') |h %> </TD>
       <TD>
@@ -128,9 +171,9 @@ function bill_now_changed (what) {
                onKeyPress = "return enable_quick_charge(event)">
       </TD>
     </TR>
-% }
+%   }
 
-<& /elements/tr-select-pkg_class.html, 'curr_value' => $cgi->param('classnum')  &>
+<& /elements/tr-select-pkg_class.html, 'curr_value' => $classnum  &>
 
 <TR>
   <TD ALIGN="right"><% mt('Invoice now') |h %></TD>
@@ -206,6 +249,8 @@ function bill_now_changed (what) {
 
 <& /elements/tr-select-taxoverride.html, 'onclick' => 'parent.taxoverridemagic(this);', 'curr_value' => $cgi->param('tax_override')  &>
 
+% } # if !$cust_pkg
+
 <TR>
   <TD ALIGN="right"><% mt('Description') |h %> </TD>
   <TD>
@@ -226,11 +271,7 @@ function bill_now_changed (what) {
 </TR>
 
 % my $row = 0;
-%   if ( $cgi->param('error') || $cgi->param('magic') ) {
-%     my $param = $cgi->Vars;
-%
-% for ( $row = 0; exists($param->{"description$row"}); $row++ ) { 
-
+% foreach (@description) {
     <TR>
       <TD></TD>
       <TD>
@@ -238,21 +279,25 @@ function bill_now_changed (what) {
                NAME       = "description<% $row %>"
                SIZE       = "60"
                MAXLENGTH  = "65"
-               VALUE      = "<% $param->{"description$row"} |h %>"
+               VALUE      = "<% $_ |h %>"
                rownum     = "<% $row %>"
                onKeyPress = "return enable_quick_charge(event)"
                onKeyUp    = "return possiblyAddRow(event)"
         >
       </TD>
     </TR>
-% } 
+% $row++;
 % } 
 
 
 </TABLE>
 
 <BR>
-<INPUT TYPE="submit" ID="submit" NAME="submit" VALUE="<% mt('Add one-time charge') |h %>" <% $cgi->param('error') ? '' :' DISABLED' %>>
+% my $label = $cust_pkg
+%             ? emt('Modify one-time charge')
+%             : emt('Add one-time charge');
+<INPUT TYPE="submit" ID="submit" NAME="submit" VALUE="<% $label %>" \
+<% ($cgi->param('error') || $cust_pkg) ? '' :' DISABLED' %>>
 
 </FORM>
 
@@ -329,9 +374,25 @@ my $conf = new FS::Conf;
 my $date_format = $conf->config('date_format') || '%m/%d/%Y';
 my $money_char = $conf->config('money_char') || '$';
 
-$cgi->param('custnum') =~ /^(\d+)$/ or die 'illegal custnum';
-my $custnum = $1;
-my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ); #XXX agent-virt
+my ($cust_main, $cust_pkg);
+if ( $cgi->param('change_pkgnum') ) {
+  # change an existing one-time charge
+  die "access denied"
+    unless $curuser->access_right('Modify one-time charge');
+
+  $cgi->param('change_pkgnum') =~ /^(\d+)$/ or die "illegal pkgnum";
+  $cust_pkg = FS::cust_pkg->by_key($1) or die "pkgnum $1 not found";
+  $cust_main = $cust_pkg->cust_main;
+} else {
+  $cgi->param('custnum') =~ /^(\d+)$/ or die 'illegal custnum';
+  $cust_main = FS::cust_main->by_key($1) or die "custnum $1 not found";
+}
+
+my $custnum = $cust_main->custnum;
+# agent-virt
+if (!exists($curuser->agentnums_href->{$cust_main->agentnum})) {
+  die "custnum $custnum not found";
+}
 
 my $format = "%m/%d/%Y %T %z (%Z)"; #false laziness w/REAL_cust_pkg.cgi?
 my $start_date = $cust_main->next_bill_date;
@@ -360,4 +421,29 @@ if ( $cust_main->invoice_terms ) {
                       );
 }
 
+my @description;
+my %param = $cgi->Vars;
+for (my $i = 0; exists($param{"description$i"}); $i++) {
+  push @description, $param{"description$i"};
+}
+
+my $classnum;
+if ( $cgi->param('classnum') =~ /^(\d+)$/ ) {
+  $classnum = $1;
+}
+
+my $part_pkg;
+
+if ( $cust_pkg ) { # set defaults
+  $part_pkg = $cust_pkg->part_pkg;
+  $pkg ||= $part_pkg->pkg;
+  $classnum ||= $part_pkg->classnum;
+  if (!@description) {
+    for (my $i = 0; $i < ($part_pkg->option('additional_count',1) || 0); $i++) 
+    {
+      push @description, $part_pkg->option("additional_info$i",1);
+    }
+  }
+}
+
 </%init>
diff --git a/httemplate/view/cust_main/packages/package.html b/httemplate/view/cust_main/packages/package.html
index 1c8db15..e97c141 100644
--- a/httemplate/view/cust_main/packages/package.html
+++ b/httemplate/view/cust_main/packages/package.html
@@ -21,17 +21,22 @@
       <TD COLSPAN=2>
         <FONT SIZE=-1>
 
-%         unless ( $cust_pkg->get('cancel') || $opt{no_links} ) {
+%         if ( $part_pkg->freq eq '0' and !$opt{no_links} ) {
+%           # One-time charge.  Nothing you can do with this, unless:
+%           if ( $curuser->access_right('Modify one-time charge') ) {
+                ( <%onetime_change_link($cust_pkg)%> )
+                <BR>
+%           }
+%
+%         } elsif ( !$cust_pkg->get('cancel') and !$opt{no_links} ) {
 %
 %           if ( $change_from ) {
 %             # This is the target package for a future change.
 %             # Nothing you can do with it besides modify/cancel the 
 %             # future change, and that's on the current package.
-%           } elsif ( $supplemental or $part_pkg->freq eq '0' ) {
+%           } elsif ( $supplemental ) {
 %             # Supplemental packages can't be changed independently.
-%             # One-time charges don't need to be changed.
-%             # For both of those, we only show "Add comments",
-%             # and "Add invoice details".
+%             # Show only "Add comments" and "Add invoice details".
 %           } else {
 %             # the usual case: links to change package definition,
 %             # discount, and customization
@@ -320,6 +325,19 @@ sub pkg_change_link {
   );
 }
 
+sub onetime_change_link {
+  my $cust_pkg = shift;
+  my $pkgnum = $cust_pkg->pkgnum;
+  include( '/elements/popup_link-cust_pkg.html',
+    'action'      => $p. "edit/quick-charge.html?change_pkgnum=$pkgnum",
+    'label'       => emt('Modify one-time charge'),
+    'actionlabel' => emt('Modify'),
+    'cust_pkg'    => $cust_pkg,
+    'width'       => 690,
+    'height'      => 380,
+  );
+}
+
 sub pkg_change_location_link {
   my $cust_pkg = shift;
   my $pkgpart = $cust_pkg->pkgpart;
diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html
index c7bf374..73082ce 100644
--- a/httemplate/view/cust_main/payment_history.html
+++ b/httemplate/view/cust_main/payment_history.html
@@ -270,6 +270,11 @@
 %                  ? sprintf("- $money_char\%.2f", $item->{'credit'})
 %                  : '';
 %
+%  $credit ||= sprintf( "<DEL>- $money_char\%.2f</DEL>",
+%                       $item->{'void_credit'}
+%                     )
+%    if exists($item->{'void_credit'});
+%
 %  my $refund  = exists($item->{'refund'})
 %                  ? sprintf("$money_char\%.2f", $item->{'refund'})
 %                  : '';
@@ -469,6 +474,15 @@ foreach my $cust_pay_void ($cust_main->cust_pay_void) {
 
 }
 
+#voided credits 
+foreach my $cust_credit_void ($cust_main->cust_credit_void) {
+  push @history, {
+    'date'        => $cust_credit_void->_date,
+    'desc'        => include('payment_history/voided_credit.html', $cust_credit_void, %opt ),
+    'void_credit' => $cust_credit_void->amount,
+  };
+}
+
 #declined payments
 foreach my $cust_pay_pending ($cust_main->cust_pay_pending_attempt) {
   push @history, {
diff --git a/httemplate/view/cust_main/payment_history/voided_credit.html b/httemplate/view/cust_main/payment_history/voided_credit.html
new file mode 100644
index 0000000..0723a72
--- /dev/null
+++ b/httemplate/view/cust_main/payment_history/voided_credit.html
@@ -0,0 +1,25 @@
+<DEL><% emt("Credit by [_1]", $cust_credit_void->otaker, $reason ) %>\
+<% $reason |h %></DEL>
+<I>
+<% emt("voided [_1]", time2str($date_format, $cust_credit_void->void_date) )%>
+% my $void_user = $cust_credit_void->void_access_user;
+% if ($void_user) {
+<% emt('by [_1]', $void_user->username) %>
+% }
+<% $void_reason |h %>
+</I>
+<%init>
+
+my( $cust_credit_void, %opt ) = @_;
+
+my $date_format = $opt{'date_format'} || '%m/%d/%Y';
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+#my $unvoid = ''; # not yet available
+my $reason = $cust_credit_void->reason;
+$reason = " ($reason)" if $reason;
+
+my $void_reason = $cust_credit_void->void_reason;
+$void_reason = " ($void_reason)" if $void_reason;
+</%init>

commit cd365522ec4e9f1b553bb1b5096c756c1fdb1d01
Author: Mark Wells <mark at freeside.biz>
Date:   Sat Oct 12 18:43:54 2013 -0700

    sales person commission report fixes, #25255

diff --git a/FS/FS/sales.pm b/FS/FS/sales.pm
index c8604ab..82c875a 100644
--- a/FS/FS/sales.pm
+++ b/FS/FS/sales.pm
@@ -146,6 +146,7 @@ sub cust_bill_pkg {
   }
 
   qsearch({ 'table'     => 'cust_bill_pkg',
+            'select'    => 'cust_bill_pkg.*',
             'addl_from' => ' LEFT JOIN cust_bill USING ( invnum ) '.
                            ' LEFT JOIN cust_pkg  USING ( pkgnum ) '.
                            ' LEFT JOIN part_pkg  USING ( pkgpart ) '.
diff --git a/httemplate/search/sales_commission.html b/httemplate/search/sales_commission.html
index d7b7a88..b4d40ae 100644
--- a/httemplate/search/sales_commission.html
+++ b/httemplate/search/sales_commission.html
@@ -1,17 +1,22 @@
+% if ( $salesnum ) {
+<% $cgi->redirect($sales_link->[0] . $salesnum) %>
+% } else {
 <& elements/search.html,
      'title'         => $title,
      'name_singular' => 'sales person',
-#     'redirect'      => sub { #my( $sales, $cgi ) = @);
-#                              $saleslink;
-#                            },
-     'header'        => [ 'Sales person', 'Sales', 'Commission', ],
-     'fields'        => [ 'salesperson', $sales_sub, $commission_sub, ],
-     'links'         => [ '', $sales_link, $commission_link ],
-     'align'         => 'lrr',
-     'query'         => { 'table' => 'sales', },
-     'count_query'   => 'SELECT COUNT(*) FROM sales',
+     'header'        => [ 'Sales person', 'One-Time Sales', 'Recurring Sales', 'Commission', ],
+     'fields'        => [ 'salesperson',
+                          $sales_sub_maker->('setup'),
+                          $sales_sub_maker->('recur'),
+                          $commission_sub,
+                        ],
+     'links'         => [ '', $sales_link, $sales_link, $commission_link ],
+     'align'         => 'lrrr',
+     'query'         => \%query,
+     'count_query'   => $count_query,
      'disableable'   => 1,
 &>
+% }
 <%init>
 
 die "access denied"
@@ -25,6 +30,14 @@ my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, '');
 
 my $date_format = $conf->config('date_format') || '%m/%d/%Y';
 
+my %query = ( 'table' => 'sales' );
+my $count_query = "SELECT COUNT(*) FROM sales";
+
+my $salesnum;
+if ( $cgi->param('salesnum') =~ /^(\d+)$/ ) {
+  $salesnum = $1;
+}
+
 my $title = 'Sales person commission';
 $title .= ': '. time2str($date_format, $beginning). ' to '.
                 time2str($date_format, $ending)
@@ -40,19 +53,22 @@ my $sales_link = [ 'sales_pkg_class.html?'.
                    'salesnum'
                  ];
 
-my $sales_sub = sub {
-  my $sales = shift;
-
-  #efficiency improvement: ask the db for a sum instead of all the records
-  my $total_recur = 0;
-  my @cust_bill_pkg = $sales->cust_bill_pkg(
-    $beginning,
-    $ending,
-    'cust_main_sales' => $cust_main_sales,
-  );
-  $total_recur += $_->recur foreach @cust_bill_pkg;
-
-  $money_char. sprintf('%.2f', $total_recur);
+my $sales_sub_maker = sub {
+  my $field = shift;
+  sub {
+    my $sales = shift;
+
+    #efficiency improvement: ask the db for a sum instead of all the records
+    my $total = 0;
+    my @cust_bill_pkg = $sales->cust_bill_pkg(
+      $beginning,
+      $ending,
+      'cust_main_sales' => $cust_main_sales,
+    );
+    $total += $_->get($field) foreach @cust_bill_pkg;
+
+    $money_char. sprintf('%.2f', $total);
+  };
 };
 
 my $commission_sub = sub {
diff --git a/httemplate/search/sales_pkg_class.html b/httemplate/search/sales_pkg_class.html
index c57aae6..da5d512 100644
--- a/httemplate/search/sales_pkg_class.html
+++ b/httemplate/search/sales_pkg_class.html
@@ -1,10 +1,16 @@
 <& elements/search.html,
      'title'         => $title,
      'name_singular' => 'package class',
-     'header'        => [ 'Package class', 'Sales', 'Commission', ],
-     'fields'        => [ 'classname', $sales_sub, $commission_sub, ],
-     'links'         => [ '', $sales_link, $commission_link ],
-     'align'         => 'lrr',
+     'header'        => [ 'Package class',
+                          'One-Time Sales',
+                          'Recurring Sales',
+                          'Commission', ],
+     'fields'        => [ 'classname',
+                          $sales_sub_maker->('setup'),
+                          $sales_sub_maker->('recur'),
+                          $commission_sub, ],
+     'links'         => [ '', '', '', $commission_link ],
+     'align'         => 'lrrr',
      'query'         => { 'table'   => 'sales_pkg_class',
                           'hashref' => { 'salesnum' => $salesnum },
                         },
@@ -45,20 +51,23 @@ my $sales_link = [ 'cust_bill_pkg.cgi?'.
                    'classnum'
                  ];
 
-my $sales_sub = sub {
-  my $sales_pkg_class = shift;
-
-  #efficiency improvement: ask the db for a sum instead of all the records
-  my $total_recur = 0;
-  my @cust_bill_pkg = $sales->cust_bill_pkg(
-    $beginning,
-    $ending,
-    'cust_main_sales' => $cust_main_sales,
-    'classnum'        => $sales_pkg_class->classnum,
-  );
-  $total_recur += $_->recur foreach @cust_bill_pkg;
-
-  $money_char. sprintf('%.2f', $total_recur);
+my $sales_sub_maker = sub {
+  my $field = shift;
+  sub {
+    my $sales_pkg_class = shift;
+
+    #efficiency improvement: ask the db for a sum instead of all the records
+    my $total = 0;
+    my @cust_bill_pkg = $sales->cust_bill_pkg(
+      $beginning,
+      $ending,
+      'cust_main_sales' => $cust_main_sales,
+      'classnum'        => $sales_pkg_class->classnum,
+    );
+    $total += $_->get($field) foreach @cust_bill_pkg;
+
+    $money_char. sprintf('%.2f', $total);
+  };
 };
 
 my $commission_sub = sub {

commit 65f3ee811a83be47ef02e0919ec122ad4083ccbd
Author: Mark Wells <mark at freeside.biz>
Date:   Sat Oct 12 18:43:40 2013 -0700

    allow commissions based on setup fee, #23969, fallout from #6991

diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 9e3b67e..9ce2e96 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -1650,6 +1650,17 @@ sub cust_bill_pkg_recur {
   $cust_bill_pkg->recur;
 }
 
+=item unit_setup CUST_PKG
+
+Returns the setup fee for one unit of the package.
+
+=cut
+
+sub unit_setup {
+  my ($self, $cust_pkg) = @_;
+  $self->option('setup_fee') || 0;
+}
+
 =item format OPTION DATA
 
 Returns data formatted according to the function 'format' described

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/AccessRight.pm                               |    1 +
 FS/FS/Mason.pm                                     |    1 +
 FS/FS/Schema.pm                                    |   31 +++++
 FS/FS/cust_credit.pm                               |   52 ++++++++-
 FS/FS/cust_credit_void.pm                          |  134 ++++++++++++++++++++
 FS/FS/cust_main.pm                                 |   28 +++-
 FS/FS/cust_pkg.pm                                  |  122 ++++++++++++++++++
 FS/FS/part_pkg.pm                                  |   11 ++
 FS/FS/sales.pm                                     |    1 +
 FS/MANIFEST                                        |    2 +
 FS/t/{AccessRight.t => cust_credit_void.t}         |    2 +-
 httemplate/edit/process/quick-charge.cgi           |  108 ++++++++++------
 httemplate/edit/quick-charge.html                  |  114 +++++++++++++++--
 httemplate/search/sales_commission.html            |   60 ++++++----
 httemplate/search/sales_pkg_class.html             |   45 ++++---
 httemplate/view/cust_main/packages/package.html    |   28 ++++-
 httemplate/view/cust_main/payment_history.html     |   14 ++
 .../cust_main/payment_history/voided_credit.html   |   25 ++++
 18 files changed, 671 insertions(+), 108 deletions(-)
 create mode 100644 FS/FS/cust_credit_void.pm
 copy FS/t/{AccessRight.t => cust_credit_void.t} (79%)
 create mode 100644 httemplate/view/cust_main/payment_history/voided_credit.html




More information about the freeside-commits mailing list