package search on null dates, RT#73715
[freeside.git] / FS / FS / cust_pkg.pm
index 467b54d..c1039b5 100644 (file)
@@ -1792,6 +1792,8 @@ sub credit_remaining {
         # the cancellation date (can happen with advance billing). in that
         # case, use the entire recurring charge:
         my $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
+        my $max_credit = $amount
+            - $cust_bill_pkg->credited('', '', setuprecur => 'recur') || 0;
 
         # but if the cancellation happens during the interval, prorate it:
         # (XXX obey prorate_round_day here?)
@@ -1800,14 +1802,23 @@ sub credit_remaining {
                       ($edate - $time) / ($edate - $cust_bill_pkg->sdate);
         }
 
+        # if there are existing credits, don't let the sum of credits exceed
+        # the recurring charge
+        $amount = $max_credit if $amount > $max_credit;
+
         $amount = sprintf('%.2f', $amount);
 
-        push @billpkgnums, $cust_bill_pkg->billpkgnum;
-        push @amounts,     $amount;
-        push @setuprecurs, 'recur';
+        # if no time has been used and/or there are existing line item
+        # credits, we may end up not needing to credit anything.
+        if ( $amount > 0 ) {
 
-        warn "Crediting for $amount on package ".$remain_pkg->pkgnum."\n"
-          if $DEBUG;
+          push @billpkgnums, $cust_bill_pkg->billpkgnum;
+          push @amounts,     $amount;
+          push @setuprecurs, 'recur';
+
+          warn "Crediting for $amount on package ".$remain_pkg->pkgnum."\n"
+            if $DEBUG;
+        }
 
       }
 
@@ -2962,7 +2973,7 @@ sub modify_charge {
       $pkg_opt_modified = 1;
     }
   }
-  $pkg_opt_modified = 1 if (scalar(@old_additional) - 1) != $i;
+  $pkg_opt_modified = 1 if scalar(@old_additional) != $i;
   $pkg_opt{'additional_count'} = $i if $i > 0;
 
   my $old_classnum;
@@ -3115,8 +3126,6 @@ sub modify_charge {
   '';
 }
 
-
-
 use Storable 'thaw';
 use MIME::Base64;
 use Data::Dumper;
@@ -5031,6 +5040,17 @@ sub cancel_sql {
   "cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0";
 }
 
+=item ncancelled_recurring_sql
+
+Returns an SQL expression identifying un-cancelled, recurring packages.
+
+=cut
+
+sub ncancelled_recurring_sql {
+  $_[0]->recurring_sql().
+  " AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 ) ";
+}
+
 =item status_sql
 
 Returns an SQL expression to give the package status as a string.
@@ -5513,29 +5533,38 @@ sub search {
 
     foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel )) {
 
-      next unless exists($params->{$field});
+      if ( $params->{$field.'_null'} ) {
 
-      my($beginning, $ending) = @{$params->{$field}};
+        push @where, "cust_pkg.$field IS NULL";
+             # this should surely be obsoleted by now: OR cust_pkg.$field == 0 
 
-      next if $beginning == 0 && $ending == 4294967295;
+      } else {
 
-      push @where,
-        "cust_pkg.$field IS NOT NULL",
-        "cust_pkg.$field >= $beginning",
-        "cust_pkg.$field <= $ending";
-
-      $orderby ||= "ORDER BY cust_pkg.$field";
-
-      if ( $field eq 'setup' ) {
-        $exclude_change_from = 1;
-      } elsif ( $field eq 'cancel' ) {
-        $exclude_change_to = 1;
-      } elsif ( $field eq 'change_date' ) {
-        # if we are given setup and change_date ranges, and the setup date
-        # falls in _both_ ranges, then include the package whether it was 
-        # a change or not
-        $exclude_change_from = 0;
+        next unless exists($params->{$field});
+
+        my($beginning, $ending) = @{$params->{$field}};
+
+        next if $beginning == 0 && $ending == 4294967295;
+
+        push @where,
+          "cust_pkg.$field IS NOT NULL",
+          "cust_pkg.$field >= $beginning",
+          "cust_pkg.$field <= $ending";
+
+        $orderby ||= "ORDER BY cust_pkg.$field";
+
+        if ( $field eq 'setup' ) {
+          $exclude_change_from = 1;
+        } elsif ( $field eq 'cancel' ) {
+          $exclude_change_to = 1;
+        } elsif ( $field eq 'change_date' ) {
+          # if we are given setup and change_date ranges, and the setup date
+          # falls in _both_ ranges, then include the package whether it was 
+          # a change or not
+          $exclude_change_from = 0;
+        }
       }
+
     }
 
     if ($exclude_change_from) {
@@ -5548,6 +5577,7 @@ sub search {
         WHERE cust_pkg.pkgnum = changed_to_pkg.change_pkgnum
       )";
     }
+
   }
 
   $orderby ||= 'ORDER BY bill';
@@ -6187,6 +6217,32 @@ sub _upgrade_data {  # class method
     my $error = $part_pkg_link->remove_linked;
     die $error if $error;
   }
+
+  # RT#73607: canceling a package with billing addons sometimes changes its
+  # pkgpart.
+  # Find records where the last replace_new record for the package before it
+  # was canceled has a different pkgpart from the package itself.
+  my @cust_pkg = qsearch({
+    'table' => 'cust_pkg',
+    'select' => 'cust_pkg.*, h_cust_pkg.pkgpart AS h_pkgpart',
+    'addl_from' => ' JOIN (
+  SELECT pkgnum, MAX(historynum) AS historynum FROM h_cust_pkg
+    WHERE cancel IS NULL
+      AND history_action = \'replace_new\'
+    GROUP BY pkgnum
+  ) AS last_history USING (pkgnum)
+  JOIN h_cust_pkg USING (historynum)',
+    'extra_sql' => ' WHERE cust_pkg.cancel is not null
+                     AND cust_pkg.pkgpart != h_cust_pkg.pkgpart'
+  });
+  foreach my $cust_pkg ( @cust_pkg ) {
+    my $pkgnum = $cust_pkg->pkgnum;
+    warn "fixing pkgpart on canceled pkg#$pkgnum\n";
+    $cust_pkg->set('pkgpart', $cust_pkg->h_pkgpart);
+    my $error = $cust_pkg->replace;
+    die $error if $error;
+  }
+
 }
 
 # will autoload in v4+