separate setup and recur discounts, #14092
authorMark Wells <mark@freeside.biz>
Fri, 16 Oct 2015 22:32:32 +0000 (15:32 -0700)
committerMark Wells <mark@freeside.biz>
Fri, 16 Oct 2015 22:47:47 +0000 (15:47 -0700)
21 files changed:
FS/FS/Schema.pm
FS/FS/Template_Mixin.pm
FS/FS/cust_bill_pkg.pm
FS/FS/cust_bill_pkg_discount.pm
FS/FS/cust_main/Packages.pm
FS/FS/cust_pkg.pm
FS/FS/cust_pkg_discount.pm
FS/FS/discount.pm
FS/FS/part_pkg/discount_Mixin.pm
httemplate/edit/cust_pkg_discount.html
httemplate/edit/process/cust_pkg_discount.html
httemplate/edit/process/quick-cust_pkg.cgi
httemplate/elements/select-months.html [new file with mode: 0644]
httemplate/elements/tr-select-months.html
httemplate/elements/tr-select-pkg-discount.html [new file with mode: 0644]
httemplate/misc/order_pkg.html
httemplate/search/cust_bill_pkg_discount.html
httemplate/search/cust_pkg_discount.html
httemplate/view/cust_main/packages.html
httemplate/view/cust_main/packages/package.html
httemplate/view/cust_main/packages/status.html

index 479ab10..ceb347d 100644 (file)
@@ -2906,6 +2906,7 @@ sub tables_hashref {
         'otaker',        'varchar', 'NULL',    32, '', '', 
         'usernum',           'int', 'NULL',    '', '', '',
         'disabled',         'char', 'NULL',     1, '', '', 
+        'setuprecur',       'char', 'NULL',     5, '', '',
       ],
       'primary_key'  => 'pkgdiscountnum',
       'unique'       => [],
index 1a3217c..ffaef97 100644 (file)
@@ -3050,6 +3050,9 @@ sub _items_cust_bill_pkg {
     # if the current line item is waiting to go out, and the one we're about
     # to start is not bundled, then push out the current one and start a new
     # one.
+    if ( $d ) {
+      $d->{amount} = $d->{setup_amount} + $d->{recur_amount};
+    }
     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
       if ( $_ && !$cust_bill_pkg->hidden ) {
         $_->{amount}      = sprintf( "%.2f", $_->{amount} );
@@ -3485,7 +3488,8 @@ sub _items_cust_bill_pkg {
           # $item_discount->{amount} is negative
 
           if ( $d and $cust_bill_pkg->hidden ) {
-            $d->{amount}      += $item_discount->{amount};
+            $d->{setup_amount} += $item_discount->{setup_amount};
+            $d->{recur_amount} += $item_discount->{recur_amount};
           } else {
             $d = $item_discount;
             $_ = &{$escape_function}($_) foreach @{ $d->{ext_description} };
@@ -3493,27 +3497,9 @@ sub _items_cust_bill_pkg {
 
           # update the active line (before the discount) to show the 
           # original price (whether this is a hidden line or not)
-          #
-          # quotation discounts keep track of setup and recur; invoice 
-          # discounts currently don't
-          if ( exists $item_discount->{setup_amount} ) {
-
-            $s->{amount} -= $item_discount->{setup_amount} if $s;
-            $r->{amount} -= $item_discount->{recur_amount} if $r;
 
-          } else {
-
-            # $active_line is the line item hashref for the line that will
-            # show the original price
-            # (use the recur or single line for the package, unless we're 
-            # showing a setup line for a package with no recurring fee)
-            my $active_line = $r;
-            if ( $type eq 'S' ) {
-              $active_line = $s;
-            }
-            $active_line->{amount} -= $item_discount->{amount};
-
-          }
+          $s->{amount} -= $item_discount->{setup_amount} if $s;
+          $r->{amount} -= $item_discount->{recur_amount} if $r;
 
         } # if there are any discounts
       } # if this is an appropriate place to show discounts
@@ -3522,6 +3508,11 @@ sub _items_cust_bill_pkg {
 
   }
 
+  # discount amount is internally split up
+  if ( $d ) {
+    $d->{amount} = $d->{setup_amount} + $d->{recur_amount};
+  }
+
   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
     if ( $_  ) {
       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
index 1780426..5861ee4 100644 (file)
@@ -820,6 +820,8 @@ quantity.
 
 sub _item_discount {
   my $self = shift;
+  my %options = @_;
+
   my @pkg_discounts = $self->pkg_discount;
   return if @pkg_discounts == 0;
   # special case: if there are old "discount details" on this line item, don't
@@ -832,7 +834,8 @@ sub _item_discount {
   my $d = {
     _is_discount    => 1,
     description     => $self->mt('Discount'),
-    amount          => 0,
+    setup_amount    => 0,
+    recur_amount    => 0,
     ext_description => \@ext,
     pkgpart         => $self->pkgpart,
     feepart         => $self->feepart,
@@ -840,9 +843,11 @@ sub _item_discount {
   };
   foreach my $pkg_discount (@pkg_discounts) {
     push @ext, $pkg_discount->description;
-    $d->{amount} -= $pkg_discount->amount;
+    my $setuprecur = $pkg_discount->cust_pkg_discount->setuprecur;
+    $d->{$setuprecur.'_amount'} -= $pkg_discount->amount;
   } 
-  $d->{amount} *= $self->quantity || 1;
+  $d->{setup_amount} *= $self->quantity || 1; # ??
+  $d->{recur_amount} *= $self->quantity || 1; # ??
   
   return $d;
 }
index 9e64d20..616657a 100644 (file)
@@ -135,10 +135,36 @@ Returns a string describing the discount (for use on an invoice).
 sub description {
   my $self = shift;
   my $discount = $self->cust_pkg_discount->discount;
+
+  if ( $self->months == 0 ) {
+    # then this is a setup discount
+    my $desc = $discount->name;
+    if ( $desc ) {
+      $desc .= ': ';
+    } else {
+      $desc = $self->mt('Setup discount of ');
+    }
+    if ( (my $percent = $discount->percent) > 0 ) {
+      $percent = sprintf('%.1f', $percent) if $percent > int($percent);
+      $percent =~ s/\.0+$//;
+      $desc .= $percent . '%';
+    } else {
+      # note "$self->amount", not $discount->amount. if a flat discount
+      # is applied to the setup fee, show the amount actually discounted.
+      # we might do this for all types of discounts.
+      my $money_char = FS::Conf->new->config('money_char') || '$';
+      $desc .= $money_char . sprintf('%.2f', $self->amount);
+    }
+  
+    # don't show "/month", months remaining or used, etc., as for setup
+    # discounts it doesn't matter.
+    return $desc;
+  }
+
   my $desc = $discount->description_short;
   $desc .= $self->mt(' each') if $self->cust_bill_pkg->quantity > 1;
 
-  if ($discount->months) {
+  if ( $discount->months and $self->months > 0 ) {
     # calculate months remaining on this cust_pkg_discount after this invoice
     my $date = $self->cust_bill_pkg->cust_bill->_date;
     my $used = FS::Record->scalar_sql(
@@ -152,7 +178,7 @@ sub description {
     $used ||= 0;
     my $remaining = sprintf('%.2f', $discount->months - $used);
     $desc .= $self->mt(' for [quant,_1,month] ([quant,_2,month] remaining)',
-              $self->months,
+              sprintf('%.2f', $self->months),
               $remaining
              );
   }
index c147e55..ead97f2 100644 (file)
@@ -197,7 +197,7 @@ sub order_pkg {
         map { $_ => $cust_pkg->$_() }
           qw( pkgbatch
               start_date order_date expire adjourn contract_end
-              refnum discountnum waive_setup
+              refnum setup_discountnum recur_discountnum waive_setup
             )
     });
     $error = $self->order_pkg('cust_pkg'    => $pkg,
index 279205b..d741907 100644 (file)
@@ -425,7 +425,7 @@ sub insert {
     }
   }
 
-  if ( $self->discountnum ) {
+  if ( $self->setup_discountnum || $self->recur_discountnum ) {
     my $error = $self->insert_discount();
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
@@ -4318,13 +4318,10 @@ sub insert_reason {
 Associates this package with a discount (see L<FS::cust_pkg_discount>, possibly
 inserting a new discount on the fly (see L<FS::discount>).
 
-Available options are:
-
-=over 4
-
-=item discountnum
-
-=back
+This will look at the cust_pkg for a pseudo-field named "setup_discountnum",
+and if present, will create a setup discount. If the discountnum is -1,
+a new discount definition will be inserted using the value in
+"setup_discountnum_amount" or "setup_discountnum_percent". Likewise for recur.
 
 If there is an error, returns the error, otherwise returns false.
 
@@ -4334,21 +4331,29 @@ sub insert_discount {
   #my ($self, %options) = @_;
   my $self = shift;
 
-  my $cust_pkg_discount = new FS::cust_pkg_discount {
-    'pkgnum'      => $self->pkgnum,
-    'discountnum' => $self->discountnum,
-    'months_used' => 0,
-    'end_date'    => '', #XXX
-    #for the create a new discount case
-    '_type'       => $self->discountnum__type,
-    'amount'      => $self->discountnum_amount,
-    'percent'     => $self->discountnum_percent,
-    'months'      => $self->discountnum_months,
-    'setup'      => $self->discountnum_setup,
-    #'disabled'    => $self->discountnum_disabled,
-  };
+  foreach my $x (qw(setup recur)) {
+    if ( my $discountnum = $self->get("${x}_discountnum") ) {
+      my $cust_pkg_discount = FS::cust_pkg_discount->new( {
+        'pkgnum'      => $self->pkgnum,
+        'discountnum' => $discountnum,
+        'setuprecur'  => $x,
+        'months_used' => 0,
+        'end_date'    => '', #XXX
+        #for the create a new discount case
+        'amount'      => $self->get("${x}_discountnum_amount"),
+        'percent'     => $self->get("${x}_discountnum_percent"),
+        'months'      => $self->get("${x}_discountnum_months"),
+      } );
+      if ( $x eq 'setup' ) {
+        $cust_pkg_discount->setup('Y');
+        $cust_pkg_discount->months('');
+      }
+      my $error = $cust_pkg_discount->insert;
+      return $error if $error;
+    }
+  }
 
-  $cust_pkg_discount->insert;
+  '';
 }
 
 =item set_usage USAGE_VALUE_HASHREF 
index 5d0f85b..aa89816 100644 (file)
@@ -59,6 +59,9 @@ end_date
 
 order taker, see L<FS::access_user>
 
+=item setuprecur
+
+whether this discount applies to setup fees or recurring fees
 
 =back
 
@@ -125,11 +128,29 @@ sub check {
     || $self->ut_alphan('otaker')
     || $self->ut_numbern('usernum')
     || $self->ut_enum('disabled', [ '', 'Y' ] )
+    || $self->ut_enum('setuprecur', [ 'setup', 'recur' ] )
   ;
   return $error if $error;
 
-  return "Discount does not apply to setup fees, and package has no recurring"
-    if ! $self->discount->setup && $self->cust_pkg->part_pkg->freq =~ /^0/;
+  my $cust_pkg = $self->cust_pkg;
+  my $discount = $self->discount;
+  if ( $self->setuprecur eq 'setup' ) {
+    if ( !$discount->setup ) {
+      # UI prevents this, and historical discounts should never have it either
+      return "Discount #".$self->discountnum." can't be applied to setup fees.";
+    } elsif ( $cust_pkg->base_setup == 0 ) {
+      # and this
+      return "Can't apply setup discount to a package with no setup fee.";
+    }
+    # else we're good. do NOT disallow applying setup discounts when the
+    # setup date is already set; upgrades use that.
+  } else {
+    if ( $self->cust_pkg->base_recur == 0 ) {
+      return "Can't apply recur discount to a package with no recurring fee.";
+    } elsif ( $cust_pkg->part_pkg->freq eq '0' ) {
+      return "Can't apply recur discount to a one-time charge.";
+    }
+  }
 
   $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
 
@@ -205,6 +226,45 @@ sub status {
 sub _upgrade_data {  # class method
   my ($class, %opts) = @_;
   $class->_upgrade_otaker(%opts);
+
+  # #14092: set setuprecur field on discounts. if we get one that applies to
+  # both setup and recur, split it into two discounts.
+  my $search = FS::Cursor->new({
+      table   => 'cust_pkg_discount',
+      hashref => { setuprecur => '' }
+  });
+  while ( my $cust_pkg_discount = $search->fetch ) {
+    my $discount = $cust_pkg_discount->discount;
+    my $cust_pkg = $cust_pkg_discount->cust_pkg;
+    # 1. Does it apply to the setup fee?
+    # Yes, if: the discount applies to setup fees generally, and the package
+    # has a setup fee.
+    # No, if: the discount is a flat amount, and is not first-month only.
+    if ( $discount->setup
+        and $cust_pkg->base_setup > 0
+        and ($discount->amount == 0 or $discount->months == 1)
+       )
+    {
+      # then clone this discount into a new one
+      my $setup_discount = FS::cust_pkg_discount->new({
+          $cust_pkg_discount->hash,
+          setuprecur      => 'setup',
+          pkgdiscountnum  => ''
+      });
+      my $error = $setup_discount->insert;
+      die "$error (migrating cust_pkg_discount to setup discount)" if $error;
+    }
+    # 2. Does it apply to the recur fee?
+    # Yes, if: the package has a recur fee.
+    if ( $cust_pkg->base_recur > 0 ) {
+      # then modify this discount in place
+      $cust_pkg_discount->set('setuprecur' => 'recur');
+      my $error = $cust_pkg_discount->replace;
+      die "$error (migrating cust_pkg_discount)" if $error;
+    }
+    # not in here yet: splitting the cust_bill_pkg_discount records.
+    # (not really necessary)
+  }
 }
 
 =back
index e113357..13146a9 100644 (file)
@@ -119,12 +119,12 @@ sub check {
 
   if ( $self->_type eq 'Select discount type' ) {
     return 'Please select a discount type';
-  } elsif ( $self->_type eq 'Amount' ) {
-    $self->percent('0');
-    return 'Amount must be greater than 0' unless $self->amount > 0;
-  } elsif ( $self->_type eq 'Percentage' ) {
-    $self->amount('0.00');
-    return 'Percentage must be greater than 0' unless $self->percent > 0;
+  } elsif ( $self->amount > 0 ) {
+    $self->set('percent', '0');
+  } elsif ( $self->percent > 0 ) {
+    $self->set('amount', '0.00');
+  } else {
+    return "Discount amount or percentage must be > 0";
   }
 
   my $error = 
index 5de7d8e..1e39f6a 100644 (file)
@@ -50,6 +50,9 @@ sub calc_discount {
   my $tot_discount = 0;
   #UI enforces just 1 for now, will need ordering when they can be stacked
 
+  # discount setup/recur splitting DOES NOT TOUCH THIS YET.
+  # we need some kind of monitoring to see who if anyone still uses term
+  # discounts.
   if ( $param->{freq_override} ) {
     # When a customer pays for more than one month at a time to receive a 
     # term discount, freq_override is set to the number of months.
@@ -80,6 +83,13 @@ sub calc_discount {
   }
 
   my @cust_pkg_discount = $cust_pkg->cust_pkg_discount_active;
+
+  if ( defined $param->{'setup_charge'} ) {
+    @cust_pkg_discount = grep { $_->setuprecur eq 'setup' } @cust_pkg_discount;
+  } else {
+    @cust_pkg_discount = grep { $_->setuprecur eq 'recur' } @cust_pkg_discount;
+  }
+    
   foreach my $cust_pkg_discount ( @cust_pkg_discount ) {
     my $discount_left;
     my $discount = $cust_pkg_discount->discount;
@@ -115,23 +125,17 @@ sub calc_discount {
         # if it's a flat amount discount for other than one month:
         # - skip the discount. unsure, leaving it alone for now.
 
-        next unless $discount->setup;
-
         $months = 0; # never count a setup discount as a month of discount
                      # (the recur discount in the same month should do it)
 
         if ( $discount->percent > 0 ) {
             $amount = $discount->percent * $param->{'setup_charge'} / 100;
-        } elsif ( $discount->amount > 0 && ($discount->months || 0) == 1) {
+        } elsif ( $discount->amount > 0 ) {
             # apply the discount amount, up to a maximum of the setup charge
             $amount = min($discount->amount, $param->{'setup_charge'});
             $discount_left = sprintf('%.2f', $discount->amount - $amount);
             # transfer remainder of discount, if any, to recur
             $param->{'discount_left_recur'}{$discount->discountnum} = $discount_left;
-        } else {
-          # I guess we don't allow multiple-month flat amount discounts to
-          # apply to setup?
-            next; 
         }
 
     } else {
@@ -180,20 +184,27 @@ sub calc_discount {
       #    recur discount is zero. 
       #}
 
-      # transfer remainder of discount, if any, to setup
-      # this is used when the recur phase wants to add a setup fee
+      # Transfer remainder of discount, if any, to setup
+      # This is used when the recur phase wants to add a setup fee
       # (prorate_defer_bill): the "discount_left_setup" amount will
-      # be subtracted in _make_lines.
-      if ( $discount->setup && $discount->amount > 0
-          && ($discount->months || 0) != 1
-         )
+      # be subtracted in _make_lines. 
+      if ( $discount->amount > 0 && ($discount->months || 0) != 1 )
       {
-        # $amount is no longer permonth at this point! correct. very good.
-        $discount_left = $amount - $recur_charge; # backward, as above
-        if ( $discount_left > 0 ) {
-          $amount = $recur_charge;
-          $param->{'discount_left_setup'}{$discount->discountnum} = 
-            0 - $discount_left;
+        # make sure there is a setup discount with this discountnum
+        # on the same package.
+        if ( qsearchs('cust_pkg_discount', {
+              pkgnum      => $cust_pkg->pkgnum,
+              discountnum => $discount->discountnum,
+              setuprecur  => 'setup'
+            }) )
+        {
+          # $amount is no longer permonth at this point! correct. very good.
+          $discount_left = $amount - $recur_charge; # backward, as above
+          if ( $discount_left > 0 ) {
+            $amount = $recur_charge;
+            $param->{'discount_left_setup'}{$discount->discountnum} = 
+              0 - $discount_left;
+          }
         }
       }
 
@@ -210,7 +221,7 @@ sub calc_discount {
         };
       }
 
-    }
+    } # else not 'setup_charge'
 
     $amount = sprintf('%.2f', $amount + 0.00000001 ); #so 1.005 rounds to 1.01
 
@@ -221,7 +232,7 @@ sub calc_discount {
       'pkgdiscountnum' => $cust_pkg_discount->pkgdiscountnum,
       'amount'         => $amount,
       'months'         => $months,
-      # XXX should have a 'setuprecur'
+      # 'setuprecur' is implied by the cust_pkg_discount link
     };
     push @{ $param->{'discounts'} }, $cust_bill_pkg_discount;
     $tot_discount += $amount;
index 0bb84b8..e1e3dae 100755 (executable)
@@ -1,18 +1,5 @@
-<% include('/elements/header-popup.html', "Discount Package") %>
-
-<SCRIPT TYPE="text/javascript">
-
-  function enable_discount_pkg () {
-    if ( document.DiscountPkgForm.discountnum.selectedIndex > 0 ) {
-      document.DiscountPkgForm.submit.disabled = false;
-    } else {
-      document.DiscountPkgForm.submit.disabled = true;
-    }
-  }
-
-</SCRIPT>
-
-<% include('/elements/error.html') %>
+<& /elements/header-popup.html, "Discount Package" &>
+<& /elements/error.html &>
 
 <FORM NAME="DiscountPkgForm" ACTION="<% $p %>edit/process/cust_pkg_discount.html" METHOD=POST>
 <INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
     </TD>
   </TR>
 
-<% include('/elements/tr-select-discount.html',
-             'empty_label' => ( $pkgdiscountnum ? '' : 'Select discount' ),
-             'onchange'    => 'enable_discount_pkg()',
-             'cgi'         => $cgi,
-          )
-%>
-
+<& /elements/tr-select-pkg-discount.html,
+  curr_value_setup  => $setup_discountnum,
+  curr_value_recur  => $recur_discountnum,
+  disable_setup     => $disable_setup,
+  disable_recur     => $disable_recur,
+&>
+  
 </TABLE>
 
 <BR>
-<INPUT NAME="submit" TYPE="submit" VALUE="Discount package" <% $pkgdiscountnum ? '' : 'DISABLED' %>>
+<INPUT NAME="submit" TYPE="submit" VALUE="Discount package">
 
 </FORM>
 </BODY>
 
 <%init>
 
-#some false laziness w/misc/change_pkg.cgi
-
 my $conf = new FS::Conf;
 
 my $curuser = $FS::CurrentUser::CurrentUser;
 
 die "access denied"
-  unless $curuser->access_right('Discount customer package');
+  unless $curuser->access_right([ 'Discount customer package',
+                                  'Waive setup fee']);
 
 my $pkgnum = scalar($cgi->param('pkgnum'));
 $pkgnum =~ /^(\d+)$/ or die "illegal pkgnum $pkgnum";
@@ -67,10 +53,30 @@ my $cust_pkg =
     'extra_sql' => ' AND '. $curuser->agentnums_sql,
   }) or die "unknown pkgnum $pkgnum";
 
-#my $cust_main = $cust_pkg->cust_main
-#  or die "can't get cust_main record for custnum ". $cust_pkg->custnum.
-#         " ( pkgnum ". cust_pkg->pkgnum. ")";
-
 my $part_pkg = $cust_pkg->part_pkg;
 
+my @discounts = $cust_pkg->cust_pkg_discount_active;
+my ($setup_discountnum, $recur_discountnum);
+foreach (@discounts) {
+  if ( $_->setuprecur eq 'setup') {
+    die "multiple setup discounts on pkg#$pkgnum" if $setup_discountnum;
+    $setup_discountnum = $_->discountnum;
+  } elsif ( $_->setuprecur eq 'recur' ) {
+    die "multiple setup discounts on pkg#$pkgnum" if $recur_discountnum;
+    $recur_discountnum = $_->discountnum;
+  }
+}
+if ( $cust_pkg->waive_setup ) {
+  $setup_discountnum = -2;
+}
+
+my $disable_setup = 1;
+if ( !$cust_pkg->get('setup') and $cust_pkg->base_setup > 0 ) {
+  $disable_setup = 0;
+}
+my $disable_recur = 1;
+if ( $cust_pkg->base_recur > 0 ) {
+  $disable_recur = 0;
+}
+
 </%init>
index 4a71f69..143611e 100644 (file)
@@ -14,9 +14,8 @@
 <%init>
 
 my $curuser = $FS::CurrentUser::CurrentUser;
-
-die "access denied"
-  unless $curuser->access_right('Discount customer package');
+my $can_discount = $curuser->access_right('Discount customer package');
+my $can_waive_setup = $curuser->access_right('Waive setup fee');
 
 #this search is really for security wrt agent virt...
 #maybe move it to the cust_pkg_discount->insert call?
@@ -29,20 +28,81 @@ my $cust_pkg = qsearchs({
 });
 die 'unknown pkgnum' unless $cust_pkg;
 
-my $cust_pkg_discount = new FS::cust_pkg_discount {
-  'pkgnum'      => $cust_pkg->pkgnum,
-  'discountnum' => scalar($cgi->param('discountnum')),
-  'months_used' => 0,
-  'end_date'    => '', #XXX
-  #for the create a new discount case
-  '_type'       => scalar($cgi->param('discountnum__type')),
-  'amount'      => scalar($cgi->param('discountnum_amount')),
-  'percent'     => scalar($cgi->param('discountnum_percent')),
-  'months'      => scalar($cgi->param('discountnum_months')),
-  'setup'       => scalar($cgi->param('discountnum_setup')),
-  #'linked'       => scalar($cgi->param('discountnum_linked')),
-  #'disabled'    => $self->discountnum_disabled,
-};
-my $error = $cust_pkg_discount->insert;
+my $error;
+my %discountnum = (setup => '', recur => '');
+if ( $cgi->param('setup_discountnum') == -2 ) {
+
+  die "access denied" unless $can_waive_setup; # UI protects against this
+  # waive setup fee (not really a discount but treated as one in the UI)
+  if ( !$cust_pkg->get('setup') and !$cust_pkg->waive_setup ) {
+    $cust_pkg->set('waive_setup' => 'Y');
+    $error = $cust_pkg->replace;
+  }
+
+} else {
+  if ( $cgi->param('setup_discountnum') =~ /^(-?\d+)$/ ) {
+    $discountnum{setup} = $1;
+  }
+  if ( $cust_pkg->waive_setup ) {
+    $cust_pkg->set('waive_setup', '');
+    $error = $cust_pkg->replace;
+  }
+}
+
+if ( $cgi->param('recur_discountnum') =~ /^(-?\d+)$/ ) {
+
+  $discountnum{recur} = $1;
+
+}
+
+my @active_discounts = $cust_pkg->cust_pkg_discount_active;
+
+foreach my $setuprecur (qw(setup recur)) {
+
+  if ( $cust_pkg->get('setup') and $setuprecur eq 'setup' ) {
+    # no point allowing setup discounts to be edited for a previously setup
+    # package
+    next;
+  }
+
+  my ($active) = grep { $_->setuprecur eq $setuprecur } @active_discounts;
+
+  if ( $active ) {
+    if ( $active->discount ne $discountnum{$setuprecur} ) {
+      $active->set('disabled' => 'Y');
+      $error ||= $active->replace;
+      undef $active;
+    } else {
+      # it's the same discountnum; don't touch it
+      next;
+    }
+  }
+
+  if ( $discountnum{$setuprecur} ) {
+    die "access_denied" unless $can_discount;
+    my $cust_pkg_discount = FS::cust_pkg_discount->new({
+      'pkgnum'      => $cust_pkg->pkgnum,
+      'discountnum' => $discountnum{$setuprecur},
+      'setuprecur'  => $setuprecur,
+      'months_used' => 0,
+      'end_date'    => '', #XXX
+      #for the create a new discount case
+      '_type'       => scalar($cgi->param($setuprecur.'_discountnum__type')),
+      'amount'      => scalar($cgi->param($setuprecur.'_discountnum_amount')),
+      'percent'     => scalar($cgi->param($setuprecur.'_discountnum_percent')),
+    });
+    if ( $setuprecur eq 'setup' ) {
+      $cust_pkg_discount->set('setup' => 'Y');
+      $cust_pkg_discount->set('months' => 1);
+    } else {
+      if ( $cgi->param($setuprecur.'_discountnum_months') =~ /^(\w+)$/ ) {
+        $cust_pkg_discount->set('months' => $1);
+      }
+    }
+
+    $error ||= $cust_pkg_discount->insert;
+
+  }
+} # foreach $setuprecur
 
 </%init>
index f1d8c26..6035215 100644 (file)
@@ -79,9 +79,6 @@ my $contactnum = $1;
 $cgi->param('locationnum') =~ /^(\-?\d*)$/
   or die 'illegal locationnum '. $cgi->param('locationnum');
 my $locationnum = $1;
-$cgi->param('discountnum') =~ /^(\-?\d*)$/
-  or die 'illegal discountnum '. $cgi->param('discountnum');
-my $discountnum = $1;
 
 # for going right to a provision service after ordering a package
 my( $svcpart, $part_svc ) = ( '', '' );
@@ -114,19 +111,29 @@ my %hash = (
     'refnum'               => $refnum,
     'contactnum'           => $contactnum,
     'locationnum'          => $locationnum,
-    'discountnum'          => $discountnum,
-    #for the create a new discount case
-    'discountnum__type'    => scalar($cgi->param('discountnum__type')),
-    'discountnum_amount'   => scalar($cgi->param('discountnum_amount')),
-    'discountnum_percent'  => scalar($cgi->param('discountnum_percent')),
-    'discountnum_months'   => scalar($cgi->param('discountnum_months')),
-    'discountnum_setup'    => scalar($cgi->param('discountnum_setup')),
     'contract_end'         => ( scalar($cgi->param('contract_end'))
                                   ? parse_datetime($cgi->param('contract_end'))
                                   : ''
                               ),
-     'waive_setup'         => ( $cgi->param('waive_setup') eq 'Y' ? 'Y' : '' ),
 );
+
+if ( $cgi->param('setup_discountnum') =~ /^(-?\d+)$/ ) { 
+  if ( $1 == -2 ) {
+    $hash{waive_setup} = 'Y';
+  } else {
+    $hash{setup_discountnum} = $1;
+    $hash{setup_discountnum_amount} = $cgi->param('setup_discountnum_amount');
+    $hash{setup_discountnum_percent} = $cgi->param('setup_discountnum_percent');
+  }
+}
+
+if ( $cgi->param('recur_discountnum') =~ /^(-?\d+)$/ ) { 
+  $hash{recur_discountnum} = $1;
+  $hash{recur_discountnum_amount} = $cgi->param('recur_discountnum_amount');
+  $hash{recur_discountnum_percent} = $cgi->param('recur_discountnum_percent');
+  $hash{recur_discountnum_months} = $cgi->param('recur_discountnum_months');
+}
+
 $hash{'custnum'} = $cust_main->custnum if $cust_main;
 
 if ( $cgi->param('start') eq 'on_hold' ) {
diff --git a/httemplate/elements/select-months.html b/httemplate/elements/select-months.html
new file mode 100644 (file)
index 0000000..1cd72fc
--- /dev/null
@@ -0,0 +1,11 @@
+<%init>
+my %opt = @_;
+$opt{id} ||= $opt{field}; # should be the default everywhere
+my $max = $opt{max} || 36;
+$opt{options} = [ '', 1 .. $max ];
+$opt{labels} = { '' => '',
+                 map { $_ => emt('[quant,_1,month]', $_) } 1 .. $max
+               };
+
+</%init>
+<& select.html, %opt &>
index b90ce1e..4d85764 100644 (file)
@@ -1,11 +1,5 @@
-<%init>
-my %opt = @_;
-$opt{id} ||= $opt{field}; # should be the default everywhere
-my $max = $opt{max} || 36;
-$opt{options} = [ '', 1 .. $max ];
-$opt{labels} = { '' => '',
-                 map { $_ => emt('[quant,_1,month]', $_) } 1 .. $max
-               };
-
-</%init>
-<& tr-select.html, %opt &>
+<& tr-td-label.html, @_ &>
+  <td>
+    <& select-months.html, @_ &>
+  </td>
+</tr>
diff --git a/httemplate/elements/tr-select-pkg-discount.html b/httemplate/elements/tr-select-pkg-discount.html
new file mode 100644 (file)
index 0000000..dc38cff
--- /dev/null
@@ -0,0 +1,196 @@
+<%doc>
+
+In order_pkg.html or similar:
+
+<& /elements/tr-select-pkg-discount.html,
+  curr_value_setup => ($cgi->param('setup_discountnum') || ''),
+  curr_value_recur => ($cgi->param('recur_discountnum') || ''),
+  disable_setup    => 0,
+  disable_recur    => 0,
+&>
+
+This provides the following:
+- If the user can waive setup fees or apply a discount, they get a 
+  select box for the setup discount, with "Waive setup fee" as an option.
+- If they can custom discount, they will also get "Custom discount" as an
+  option. If selected, this will show fields to enter the custom discount
+  amount/percentage.
+- If they can waive setup fees but NOT apply a discount, they only get a
+  checkbox to waive setup fee.
+- Same for recurring fee, but without the "waive setup fee" stuff, obviously.
+- Custom recurring discounts also have an option for a duration in months.
+
+"disable_setup" locks the setup discount, but will still show a static
+description if curr_value_setup is set. Likewise "disable_recur".
+
+</%doc>
+% # SETUP DISCOUNT
+
+% # select-discount knows about the "custom discount" ACL
+% if ( $curuser->access_right('Discount customer package')
+%      and !$opt{disable_setup} )
+% {
+%   my $pre_options = [ '' => '(none)' ];
+%   if ( $curuser->access_right('Waive setup fee') ) {
+%     push @$pre_options, -2 => 'Waive setup fee';
+%   }
+<& tr-td-label.html, label => emt('Setup fee') &>
+  <td>
+    <& select-discount.html,
+      field       => 'setup_discountnum',
+      id          => 'setup_discountnum',
+      hashref     =>  { disabled => '',
+                        setup    => 'Y'
+                      },
+      extra_sql   =>  ' AND (percent > 0 OR months = 1)',
+      curr_value  => $opt{'curr_value_setup'},
+      disable_empty => 1,
+      pre_options => $pre_options,
+    &>
+  </td>
+</tr>
+% # custom discount
+<tr class="setup_discount_custom">
+  <td></td>
+  <td>Amount <% $money_char %>
+    <& input-text.html,
+      field       => 'setup_discountnum_amount',
+      curr_value  => ($cgi->param('setup_discountnum_amount') || ''),
+      size        => 5,
+    &>
+  or percentage
+    <& input-text.html,
+      field       => 'setup_discountnum_percent',
+      curr_value  => ($cgi->param('setup_discountnum_percent') || ''),
+      size        => 5,
+    &> %
+  </td>
+</tr>
+
+% } elsif ( $curuser->access_right('Waive setup fee')
+%           and !$opt{disable_setup} )
+% {
+
+<& tr-td-label.html, label => emt('Waive setup fee') &>
+  <td>
+  <& checkbox.html,
+      field       => 'setup_discountnum',
+      id          => 'setup_discountnum',
+      value       => '-2',
+      curr_value  => $opt{'curr_value_setup'},
+  &>
+  </td>
+</tr>
+
+% } elsif ( $opt{'curr_value_setup'} ) { # user can't do anything
+%
+%   my $discount = FS::discount->by_key($opt{'curr_value_setup'});
+
+  <INPUT TYPE="hidden" NAME="setup_discountnum" VALUE="<% $opt{curr_value_setup} %>">
+
+  <% $discount->description_short %>
+
+% }
+
+% # RECUR DISCOUNT
+
+% if ( $curuser->access_right('Discount customer package')
+%      and !$opt{disable_recur} ) {
+
+<& tr-td-label.html, label => emt('Recurring fee') &>
+  <td>
+    <& select-discount.html,
+      field       => 'recur_discountnum',
+      id          => 'recur_discountnum',
+      hashref     =>  { disabled => '' },
+      curr_value  => $opt{'curr_value_recur'},
+    &>
+
+  </td>
+</tr>
+% # custom discount
+<tr class="recur_discount_custom">
+  <td></td>
+  <td>Amount <% $money_char %>
+    <& input-text.html,
+      field       => 'recur_discountnum_amount',
+      curr_value  => ($cgi->param('recur_discountnum_amount') || ''),
+      size        => 5,
+    &>
+  or percentage
+    <& input-text.html,
+      field       => 'recur_discountnum_percent',
+      curr_value  => ($cgi->param('recur_discountnum_percent') || ''),
+      size        => 5,
+    &> %
+  </td>
+</tr>
+<tr class="recur_discount_custom">
+  <td></td>
+  <td>Expires after
+    <& /elements/select-months.html,
+      field       => 'recur_discountnum_months',
+      curr_value  => ($cgi->param('recur_discountnum_months') || ''),
+    &>
+  </td>
+</tr>
+
+% } elsif ( $opt{'curr_value_recur'} ) {
+%
+%   my $discount = FS::discount->by_key($opt{'curr_value_recur'});
+
+  <INPUT TYPE="hidden" NAME="recur_discountnum" VALUE="<% $opt{curr_value_recur} %>">
+
+  <% $discount->description %>
+
+% }
+
+<SCRIPT TYPE="text/javascript">
+$(document).ready(function() {
+  ['setup', 'recur'].forEach(function(x) {
+    var discountnum = $('#'+x+'_discountnum');
+
+    // if it's been set to a custom discount, show custom discount inputs
+    var discountnum_changed = function() {
+      var val = this.value;
+      var custom = $('.'+x+'_discount_custom');
+      if ( val == -1 ) {
+        custom.show();
+      } else {
+        custom.hide();
+      }
+    };
+
+    discountnum.on('change', discountnum_changed);
+    discountnum.trigger('change');
+
+    // if amount contains a value, disable percent, and vice versa
+    var amount_percent_keyup = function(event) {
+      var other = event.data;
+      if (this.value.length > 0) {
+        other.disabled = true;
+      } else {
+        other.disabled = false;
+      }
+    };
+    var amount = $('#'+x+'_discountnum_amount');
+    var percent = $('#'+x+'_discountnum_percent');
+    amount.on('keyup', percent, amount_percent_keyup);
+    percent.on('keyup', amount, amount_percent_keyup);
+
+    amount.trigger('keyup');
+    percent.trigger('keyup');
+  });
+});
+</script>
+<%init>
+
+my %opt = (
+  'curr_value_setup' => ($cgi->param('setup_discountnum') || ''),
+  'curr_value_recur' => ($cgi->param('recur_discountnum') || ''),
+  @_
+);
+my $curuser = $FS::CurrentUser::CurrentUser;
+my $money_char = FS::Conf->new->config('money_char') || '$';
+
+</%init>
index cb2bd48..e282501 100644 (file)
 % if ( $discount_cust_pkg || $waive_setup_fee ) {
   <FONT CLASS="fsinnerbox-title"><% mt('Discounting') |h %></FONT>
   <% ntable("#cccccc") %>
-
-%   if ( $waive_setup_fee ) {
-      <TR>
-        <TH ALIGN="right"><% mt('Waive setup fee') |h %> </TH>
-        <TD COLSPAN=6><INPUT TYPE="checkbox" NAME="waive_setup" VALUE="Y"></TD>
-      </TR>
-%   }
-
-%   if ( $discount_cust_pkg ) {
-      <& /elements/tr-select-discount.html,
-               'element_etc' => 'DISABLED',
-               'colspan'     => 7,
-               'cgi'         => $cgi,
-      &>
-%   }
-
+    <& /elements/tr-select-pkg-discount.html &>
   </TABLE><BR>
 
 % }
index 6da5787..c634828 100644 (file)
@@ -20,9 +20,7 @@
                    sub { $_[0]->cust_pkg_discount->discount->description },
                    sub { $_[0]->cust_pkg_discount->discount->classname },
                    sub { sprintf($money_char.'%.2f', shift->amount ) },
-                   sub { my $m = shift->months;
-                         $m =~ /\./ ? sprintf('%.2f', $m) : $m;
-                       },
+                   $months_sub,
                    'pkg',#sub { $_[0]->cust_bill_pkg->cust_pkg->part_pkg->pkg },
                    'invnum',
                    sub { time2str('%b %d %Y', shift->_date ) },
@@ -218,4 +216,11 @@ my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ];
 my $conf = new FS::Conf;
 my $money_char = $conf->config('money_char') || '$';
 
+my $months_sub = sub {
+  my $cust_bill_pkg_discount = shift;
+  return 'Setup'
+    if $cust_bill_pkg_discount->cust_pkg_discount->setuprecur eq 'setup';
+  sprintf('%.2f', $cust_bill_pkg_discount->months);
+};
+
 </%init>
index f0c7447..ab6ad2b 100644 (file)
@@ -18,9 +18,7 @@
                                      sub { ucfirst( shift->status ) },
                                      sub { shift->discount->description },
                                      sub { shift->discount->classname },
-                                     sub { my $m = shift->months_used;
-                                           $m =~ /\./ ? sprintf('%.2f',$m) : $m;
-                                         },
+                                     $months_used_sub,
                                      'otaker',
                                      'pkg',
                                      \&FS::UI::Web::cust_fields,
@@ -165,4 +163,9 @@ my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ];
 
 my $conf = new FS::Conf;
 
+my $months_used_sub = sub {
+  my $cust_pkg_discount = shift;
+  return 'Setup only' if $cust_pkg_discount->setuprecur eq 'setup';
+  return sprintf('%.2f', $cust_pkg_discount->months_used);
+};
 </%init>
index 4903e18..0c67843 100755 (executable)
@@ -42,6 +42,7 @@ table.hiddenrows {
   z-index: 1;
   text-align: center;
 }
+
 </STYLE>
 % # activate rolldown buttons for hidden package blocks
 <SCRIPT TYPE="text/javascript">
index 8aa6403..e98b95e 100644 (file)
                 (&nbsp;<%onetime_change_link($cust_pkg)%>&nbsp;)
 %           }
 %           # also, you can discount it
-%           if ( $curuser->access_right('Discount customer package')
-%                && ! scalar($cust_pkg->cust_pkg_discount_active)
-%                && ! scalar($cust_pkg->part_pkg->part_pkg_discount)
-%              ) {
+%           if ( $can_discount_pkg ) {
               (&nbsp;<%pkg_discount_link($cust_pkg)%>&nbsp;)
 %           }
           <BR>
                 (&nbsp;<%pkg_change_link($cust_pkg)%>&nbsp;)
 %             } 
 %
-%             if ( $curuser->access_right('Discount customer package')
-%                  && $part_pkg->can_discount
-%                  && ! scalar( @{ $cust_pkg->{_cust_pkg_discount_active} } )
-%                  && (    ! $opt{'term_discounts'}
-%                       || ! scalar($cust_pkg->part_pkg->part_pkg_discount)
-%                     )
-%                )
-%             {
+%             if ( $can_discount_pkg ) {
 %               $br=1;
                 (&nbsp;<%pkg_discount_link($cust_pkg)%>&nbsp;)
 %             }
@@ -437,4 +427,21 @@ sub pkg_event_link {
   '</a>';
 }
 
+# figure out if this user will be able to edit either the setup or recurring
+# discounts for this package
+my $can_discount_pkg = (
+  $part_pkg->can_discount
+  and
+  ( ( $curuser->access_right(['Discount customer package', 'Waive setup fee'])
+      and $cust_pkg->base_setup > 0
+      and !$cust_pkg->setup
+    )
+   or
+    ( $curuser->access_right('Discount customer package')
+      and $cust_pkg->base_recur > 0
+      and $cust_pkg->freq ne '0'
+    )
+  )
+);
+
 </%init>
index 7e125f7..13bd202 100644 (file)
@@ -461,10 +461,11 @@ sub pkg_status_row_changed {
     my $part_pkg = $old->part_pkg;
     $html .= pkg_status_row_colspan(
       $cust_pkg, 
-      emt("Changed from [_1]: [_2]",
-             $cust_pkg->change_pkgnum,
-             $part_pkg->pkg_comment(cust_pkg=>$old, nopartpkg=>1)
-         ),
+#      emt("Changed from [_1]: [_2]",
+#             $cust_pkg->change_pkgnum,
+#             $part_pkg->pkg_comment(cust_pkg=>$old, nopartpkg=>1)
+#         ),
+      '',
       '',
       'size'    => '-1',
       'align'   => 'right',
@@ -529,17 +530,25 @@ sub pkg_status_row_discount {
 
     my $discount = $cust_pkg_discount->discount;
 
-    my $label = '<B>'.emt('Discount').'</B>: '. $discount->description;
-    if ( $discount->months ) {
+    my $label = '<SPAN STYLE="font-size: small"><B>';
+    if ( $cust_pkg_discount->setuprecur eq 'setup' ) {
+      $label .= emt('Setup Discount');
+    } else {
+      $label .= emt('Recurring Discount');
+    }
+    $label .= '</B>: '. $discount->description;
+    warn Dumper $cust_pkg_discount;
+    if ( $discount->months > 0 and $cust_pkg_discount->months_used > 0 ) {
       my $remaining = $discount->months - $cust_pkg_discount->months_used;
       $remaining = sprintf('%.2f', $remaining) if $remaining =~ /\./;
-      $label .= emt("([_1] months remaining)",$remaining);
+      $label .= <br> . emt("([_1] months remaining)",$remaining);
     }
+    $label .= '</SPAN>';
 
-    $label .= ' <FONT SIZE="-1">('.
-                '<A HREF="../misc/delete-cust_pkg_discount.html?'.
-                  $cust_pkg_discount->pkgdiscountnum.
-                '">'.emt('remove discount').'</A>)</FONT>';
+    #$label .= ' <FONT SIZE="-1">('.
+    #            '<A HREF="../misc/delete-cust_pkg_discount.html?'.
+    #              $cust_pkg_discount->pkgdiscountnum.
+    #            '">'.emt('remove discount').'</A>)</FONT>';
 
     $html .= pkg_status_row_colspan( $cust_pkg, $label, '', %opt );