improve unsuspend behavior for packages on hold, #28508
[freeside.git] / FS / FS / part_fee.pm
index d1e6477..370005c 100644 (file)
@@ -4,8 +4,9 @@ use strict;
 use base qw( FS::o2m_Common FS::Record );
 use vars qw( $DEBUG );
 use FS::Record qw( qsearch qsearchs );
 use base qw( FS::o2m_Common FS::Record );
 use vars qw( $DEBUG );
 use FS::Record qw( qsearch qsearchs );
+use FS::cust_bill_pkg_display;
 
 
-$DEBUG = 1;
+$DEBUG = 0;
 
 =head1 NAME
 
 
 =head1 NAME
 
@@ -54,9 +55,6 @@ the invoice
 Currently, taxable fees will be treated like they exist at the customer's
 default service location.
 
 Currently, taxable fees will be treated like they exist at the customer's
 default service location.
 
-=item nextbill - 'Y' if this fee should be delayed until the customer is 
-billed for a package.
-
 =item taxclass - the tax class the fee belongs to, as a string, for the 
 internal tax system
 
 =item taxclass - the tax class the fee belongs to, as a string, for the 
 internal tax system
 
@@ -138,7 +136,6 @@ sub check {
     || $self->ut_flag('disabled')
     || $self->ut_foreign_keyn('classnum', 'pkg_class', 'classnum')
     || $self->ut_flag('taxable')
     || $self->ut_flag('disabled')
     || $self->ut_foreign_keyn('classnum', 'pkg_class', 'classnum')
     || $self->ut_flag('taxable')
-    || $self->ut_flag('nextbill')
     || $self->ut_textn('taxclass')
     || $self->ut_numbern('taxproductnum')
     || $self->ut_floatn('pay_weight')
     || $self->ut_textn('taxclass')
     || $self->ut_numbern('taxproductnum')
     || $self->ut_floatn('pay_weight')
@@ -150,23 +147,20 @@ sub check {
     || $self->ut_moneyn('minimum')
     || $self->ut_moneyn('maximum')
     || $self->ut_flag('limit_credit')
     || $self->ut_moneyn('minimum')
     || $self->ut_moneyn('maximum')
     || $self->ut_flag('limit_credit')
-    || $self->ut_enum('basis', [ '', 'charged', 'owed' ])
+    || $self->ut_enum('basis', [ 'charged', 'owed', 'usage' ])
     || $self->ut_enum('setuprecur', [ 'setup', 'recur' ])
   ;
   return $error if $error;
 
     || $self->ut_enum('setuprecur', [ 'setup', 'recur' ])
   ;
   return $error if $error;
 
-  return "For a percentage fee, the basis must be set"
-    if $self->get('percent') > 0 and $self->get('basis') eq '';
-
-  if ( ! $self->get('percent') and ! $self->get('limit_credit') ) {
-    # then it makes no sense to apply minimum/maximum
-    $self->set('minimum', '');
-    $self->set('maximum', '');
-  }
   if ( $self->get('limit_credit') ) {
     $self->set('maximum', '');
   }
 
   if ( $self->get('limit_credit') ) {
     $self->set('maximum', '');
   }
 
+  if ( $self->get('basis') eq 'usage' ) {
+    # to avoid confusion, don't also allow charging a percentage
+    $self->set('percent', 0);
+  }
+
   $self->SUPER::check;
 }
 
   $self->SUPER::check;
 }
 
@@ -182,7 +176,7 @@ sub explanation {
   my $money_char = FS::Conf->new->config('money_char') || '$';
   my $money = $money_char . '%.2f';
   my $percent = '%.1f%%';
   my $money_char = FS::Conf->new->config('money_char') || '$';
   my $money = $money_char . '%.2f';
   my $percent = '%.1f%%';
-  my $string;
+  my $string = '';
   if ( $self->amount > 0 ) {
     $string = sprintf($money, $self->amount);
   }
   if ( $self->amount > 0 ) {
     $string = sprintf($money, $self->amount);
   }
@@ -197,7 +191,14 @@ sub explanation {
     } elsif ( $self->basis('owed') ) {
       $string .= 'unpaid invoice balance';
     }
     } elsif ( $self->basis('owed') ) {
       $string .= 'unpaid invoice balance';
     }
+  } elsif ( $self->basis eq 'usage' ) {
+    if ( $string ) {
+      $string .= " plus \n";
+    }
+    # append per-class descriptions
+    $string .= join("\n", map { $_->explanation } $self->part_fee_usage);
   }
   }
+
   if ( $self->minimum or $self->maximum or $self->limit_credit ) {
     $string .= "\nbut";
     if ( $self->minimum ) {
   if ( $self->minimum or $self->maximum or $self->limit_credit ) {
     $string .= "\nbut";
     if ( $self->minimum ) {
@@ -248,37 +249,72 @@ sub lineitem {
   warn "Calculating fee: ".$self->itemdesc." on ".
     ($cust_bill->invnum ? "invoice #".$cust_bill->invnum : "current invoice").
     "\n" if $DEBUG;
   warn "Calculating fee: ".$self->itemdesc." on ".
     ($cust_bill->invnum ? "invoice #".$cust_bill->invnum : "current invoice").
     "\n" if $DEBUG;
-  if ( $self->percent > 0 and $self->basis ne '' ) {
-    warn $self->percent . "% of amount ".$self->basis.")\n"
-      if $DEBUG;
-
-    # $total_base: the total charged/owed on the invoice
-    # %item_base: billpkgnum => fraction of base amount
-    if ( $cust_bill->invnum ) {
-      my $basis = $self->basis;
-      $total_base = $cust_bill->$basis; # "charged", "owed"
+  my $basis = $self->basis;
+
+  # $total_base: the total charged/owed on the invoice
+  # %item_base: billpkgnum => fraction of base amount
+  if ( $cust_bill->invnum ) {
 
 
-      # calculate the fee on an already-inserted past invoice.  This may have 
-      # payments or credits, so if basis = owed, we need to consider those.
+    # calculate the fee on an already-inserted past invoice.  This may have 
+    # payments or credits, so if basis = owed, we need to consider those.
+    @items = $cust_bill->cust_bill_pkg;
+    if ( $basis ne 'usage' ) {
+
+      $total_base = $cust_bill->$basis; # "charged", "owed"
       my $basis_sql = $basis.'_sql';
       my $sql = 'SELECT ' . FS::cust_bill_pkg->$basis_sql .
                 ' FROM cust_bill_pkg WHERE billpkgnum = ?';
       my $basis_sql = $basis.'_sql';
       my $sql = 'SELECT ' . FS::cust_bill_pkg->$basis_sql .
                 ' FROM cust_bill_pkg WHERE billpkgnum = ?';
-      @items = $cust_bill->cust_bill_pkg;
       @item_base = map { FS::Record->scalar_sql($sql, $_->billpkgnum) }
                     @items;
       @item_base = map { FS::Record->scalar_sql($sql, $_->billpkgnum) }
                     @items;
-    } else {
-      # the fee applies to _this_ invoice.  It has no payments or credits, so
-      # "charged" and "owed" basis are both just the invoice amount, and 
-      # the line item amounts (setup + recur)
+
+      $amount += $total_base * $self->percent / 100;
+    }
+  } else {
+    # the fee applies to _this_ invoice.  It has no payments or credits, so
+    # "charged" and "owed" basis are both just the invoice amount, and 
+    # the line item amounts (setup + recur)
+    @items = @{ $cust_bill->get('cust_bill_pkg') };
+    if ( $basis ne 'usage' ) {
       $total_base = $cust_bill->charged;
       $total_base = $cust_bill->charged;
-      @items = @{ $cust_bill->get('cust_bill_pkg') };
       @item_base = map { $_->setup + $_->recur }
                     @items;
       @item_base = map { $_->setup + $_->recur }
                     @items;
-    }
 
 
-    $amount += $total_base * $self->percent / 100;
+      $amount += $total_base * $self->percent / 100;
+    }
   }
 
   }
 
+  if ( $basis eq 'usage' ) {
+
+    my %part_fee_usage = map { $_->classnum => $_ } $self->part_fee_usage;
+
+    foreach my $item (@items) { # cust_bill_pkg objects
+      my $usage_fee = 0;
+      $item->regularize_details;
+      my $details;
+      if ( $item->billpkgnum ) {
+        $details = [
+          qsearch('cust_bill_pkg_detail', { billpkgnum => $item->billpkgnum })
+        ];
+      } else {
+        $details = $item->get('details') || [];
+      }
+      foreach my $d (@$details) {
+        # if there's a usage fee defined for this class...
+        next if $d->amount eq '' # not a real usage detail
+             or $d->amount == 0  # zero charge, probably shouldn't charge fee
+        ;
+        my $p = $part_fee_usage{$d->classnum} or next;
+        $usage_fee += ($d->amount * $p->percent / 100)
+                    + $p->amount;
+        # we'd create detail records here if we were doing that
+      }
+      # bypass @item_base entirely
+      push @item_fee, $usage_fee;
+      $amount += $usage_fee;
+    }
+
+  } # if $basis eq 'usage'
+
   if ( $self->minimum ne '' and $amount < $self->minimum ) {
     warn "Applying mininum fee\n" if $DEBUG;
     $amount = $self->minimum;
   if ( $self->minimum ne '' and $amount < $self->minimum ) {
     warn "Applying mininum fee\n" if $DEBUG;
     $amount = $self->minimum;
@@ -294,7 +330,7 @@ sub lineitem {
       $maximum = -1 * $balance;
     }
   }
       $maximum = -1 * $balance;
     }
   }
-  if ( $maximum ne '' ) {
+  if ( $maximum ne '' and $amount > $maximum ) {
     warn "Applying maximum fee\n" if $DEBUG;
     $amount = $maximum;
   }
     warn "Applying maximum fee\n" if $DEBUG;
     $amount = $maximum;
   }
@@ -312,7 +348,7 @@ sub lineitem {
   });
 
   if ( $maximum and $self->taxable ) {
   });
 
   if ( $maximum and $self->taxable ) {
-    warn "Estimating taxes on fee.\n";
+    warn "Estimating taxes on fee.\n" if $DEBUG;
     # then we need to estimate tax to respect the maximum
     # XXX currently doesn't work with external (tax_rate) taxes
     # or batch taxes, obviously
     # then we need to estimate tax to respect the maximum
     # XXX currently doesn't work with external (tax_rate) taxes
     # or batch taxes, obviously
@@ -331,6 +367,7 @@ sub lineitem {
     if ($total_rate > 0) {
       my $max_cents = $maximum * 100;
       my $charge_cents = sprintf('%0.f', $max_cents * 100/(100 + $total_rate));
     if ($total_rate > 0) {
       my $max_cents = $maximum * 100;
       my $charge_cents = sprintf('%0.f', $max_cents * 100/(100 + $total_rate));
+      # the actual maximum that we can charge...
       $maximum = sprintf('%.2f', $charge_cents / 100.00);
       $amount = $maximum if $amount > $maximum;
     }
       $maximum = sprintf('%.2f', $charge_cents / 100.00);
       $amount = $maximum if $amount > $maximum;
     }
@@ -339,11 +376,19 @@ sub lineitem {
   # set the amount that we'll charge
   $cust_bill_pkg->set( $self->setuprecur, $amount );
 
   # set the amount that we'll charge
   $cust_bill_pkg->set( $self->setuprecur, $amount );
 
+  # create display record
+  my $categoryname = '';
   if ( $self->classnum ) {
     my $pkg_category = $self->pkg_class->pkg_category;
   if ( $self->classnum ) {
     my $pkg_category = $self->pkg_class->pkg_category;
-    $cust_bill_pkg->set('section' => $pkg_category->categoryname)
-      if $pkg_category;
+    $categoryname = $pkg_category->categoryname if $pkg_category;
   }
   }
+  my $displaytype = ($self->setuprecur eq 'setup') ? 'S' : 'R';
+  my $display = FS::cust_bill_pkg_display->new({
+      type    => $displaytype,
+      section => $categoryname,
+      # post_total? summary? who the hell knows?
+  });
+  $cust_bill_pkg->set('display', [ $display ]);
 
   # if this is a percentage fee and has line item fractions,
   # adjust them to be proportional and to add up correctly.
 
   # if this is a percentage fee and has line item fractions,
   # adjust them to be proportional and to add up correctly.
@@ -368,25 +413,25 @@ sub lineitem {
         }
       }
     }
         }
       }
     }
-    # and add them to the cust_bill_pkg
+  }
+  if ( @item_fee ) {
+    # add allocation records to the cust_bill_pkg
     for (my $i = 0; $i < scalar(@items); $i++) {
       if ( $item_fee[$i] > 0 ) {
         push @cust_bill_pkg_fee, FS::cust_bill_pkg_fee->new({
             cust_bill_pkg   => $cust_bill_pkg,
     for (my $i = 0; $i < scalar(@items); $i++) {
       if ( $item_fee[$i] > 0 ) {
         push @cust_bill_pkg_fee, FS::cust_bill_pkg_fee->new({
             cust_bill_pkg   => $cust_bill_pkg,
-            base_invnum     => $cust_bill->invnum,
+            base_invnum     => $cust_bill->invnum, # may be null
             amount          => $item_fee[$i],
             base_cust_bill_pkg => $items[$i], # late resolve
         });
       }
     }
             amount          => $item_fee[$i],
             base_cust_bill_pkg => $items[$i], # late resolve
         });
       }
     }
-  } else { # if !@item_base
+  } else { # if !@item_fee
     # then this isn't a proportional fee, so it just applies to the 
     # entire invoice.
     # then this isn't a proportional fee, so it just applies to the 
     # entire invoice.
-    # (if it's the current invoice, $cust_bill->invnum is null and that 
-    # will be fixed later)
     push @cust_bill_pkg_fee, FS::cust_bill_pkg_fee->new({
         cust_bill_pkg   => $cust_bill_pkg,
     push @cust_bill_pkg_fee, FS::cust_bill_pkg_fee->new({
         cust_bill_pkg   => $cust_bill_pkg,
-        base_invnum     => $cust_bill->invnum,
+        base_invnum     => $cust_bill->invnum, # may be null
         amount          => $amount,
     });
   }
         amount          => $amount,
     });
   }
@@ -448,6 +493,19 @@ sub tax_rates {
   return @taxes;
 }
 
   return @taxes;
 }
 
+=item categoryname 
+
+Returns the package category name, or the empty string if there is no package
+category.
+
+=cut
+
+sub categoryname {
+  my $self = shift;
+  my $pkg_class = $self->pkg_class;
+  $pkg_class ? $pkg_class->categoryname : '';
+}
+
 sub part_pkg_taxoverride {} # we don't do overrides here
 
 sub has_taxproduct {
 sub part_pkg_taxoverride {} # we don't do overrides here
 
 sub has_taxproduct {