option to bill partial period on cancel/suspend, #16066
authormark <mark>
Tue, 24 Jan 2012 22:25:13 +0000 (22:25 +0000)
committermark <mark>
Tue, 24 Jan 2012 22:25:13 +0000 (22:25 +0000)
FS/FS/cust_main/Billing.pm
FS/FS/cust_pkg.pm
FS/FS/part_pkg.pm
FS/FS/part_pkg/flat.pm
FS/FS/part_pkg/rt_time.pm
FS/FS/part_pkg/voip_cdr.pm
FS/FS/part_pkg/voip_inbound.pm

index 072874e..23d3b49 100644 (file)
@@ -177,7 +177,8 @@ sub cancel_expired_pkgs {
   foreach my $cust_pkg ( @cancel_pkgs ) {
     my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
     my $error = $cust_pkg->cancel($cpr ? ( 'reason'        => $cpr->reasonnum,
-                                           'reason_otaker' => $cpr->otaker
+                                           'reason_otaker' => $cpr->otaker,
+                                           'time'          => $time,
                                          )
                                        : ()
                                  );
@@ -407,7 +408,8 @@ sub bill {
 
       my $next_bill = $cust_pkg->getfield('bill') || 0;
       my $error;
-      while ( $next_bill <= $time ) {
+      # let this run once if this is the last bill upon cancellation
+      while ( $next_bill <= $time or $options{cancel} ) {
         $error =
           $self->_make_lines( 'part_pkg'            => $part_pkg,
                               'cust_pkg'            => $cust_pkg,
@@ -427,6 +429,9 @@ sub bill {
         # or if we're not incrementing the bill date.
         last if ($cust_pkg->getfield('bill') || 0) == $next_bill;
 
+        # or if we're letting it run only once
+        last if $options{cancel};
+
         $next_bill = $cust_pkg->getfield('bill') || 0;
 
         #stop if -o was passed to freeside-daily
index 54119b5..fdd30c5 100644 (file)
@@ -691,11 +691,19 @@ Available options are:
 
 =item quiet - can be set true to supress email cancellation notices.
 
-=item time -  can be set to cancel the package based on a specific future or historical date.  Using time ensures that the remaining amount is calculated correctly.  Note however that this is an immediate cancel and just changes the date.  You are PROBABLY looking to expire the account instead of using this.
+=item time -  can be set to cancel the package based on a specific future or 
+historical date.  Using time ensures that the remaining amount is calculated 
+correctly.  Note however that this is an immediate cancel and just changes 
+the date.  You are PROBABLY looking to expire the account instead of using 
+this.
 
-=item reason - can be set to a cancellation reason (see L<FS:reason>), either a reasonnum of an existing reason, or passing a hashref will create a new reason.  The hashref should have the following keys: typenum - Reason type (see L<FS::reason_type>, reason - Text of the new reason.
+=item reason - can be set to a cancellation reason (see L<FS:reason>), 
+either a reasonnum of an existing reason, or passing a hashref will create 
+a new reason.  The hashref should have the following keys: typenum - Reason 
+type (see L<FS::reason_type>, reason - Text of the new reason.
 
-=item date - can be set to a unix style timestamp to specify when to cancel (expire)
+=item date - can be set to a unix style timestamp to specify when to 
+cancel (expire)
 
 =item nobill - can be set true to skip billing if it might otherwise be done.
 
@@ -739,24 +747,27 @@ sub cancel {
     return "";  # no error
   }
 
-  my $date = $options{date} if $options{date}; # expire/cancel later
-  $date = '' if ($date && $date <= time);      # complain instead?
+  # XXX possibly set cancel_time to the expire date?
+  my $cancel_time = $options{'time'} || time;
+  my $date = $options{'date'} if $options{'date'}; # expire/cancel later
+  $date = '' if ($date && $date <= $cancel_time);      # complain instead?
 
   #race condition: usage could be ongoing until unprovisioned
   #resolved by performing a change package instead (which unprovisions) and
   #later cancelling
-  if ( !$options{nobill} && !$date && $conf->exists('bill_usage_on_cancel') ) {
+  if ( !$options{nobill} && !$date ) {
+    # && $conf->exists('bill_usage_on_cancel') ) { #calc_cancel checks this
       my $copy = $self->new({$self->hash});
       my $error =
-        $copy->cust_main->bill( pkg_list => [ $copy ], cancel => 1 );
+        $copy->cust_main->bill( 'pkg_list' => [ $copy ], 
+                                'cancel'   => 1,
+                                'time'     => $cancel_time );
       warn "Error billing during cancel, custnum ".
         #$self->cust_main->custnum. ": $error"
         ": $error"
         if $error;
   }
 
-  my $cancel_time = $options{'time'} || time;
-
   if ( $options{'reason'} ) {
     $error = $self->insert_reason( 'reason' => $options{'reason'},
                                    'action' => $date ? 'expire' : 'cancel',
@@ -790,10 +801,7 @@ sub cancel {
   }
 
   unless ($date) {
-
-    # Add a credit for remaining service
-    my $last_bill = $self->getfield('last_bill') || 0;
-    my $next_bill = $self->getfield('bill') || 0;
+    # credit remaining time if appropriate
     my $do_credit;
     if ( exists($options{'unused_credit'}) ) {
       $do_credit = $options{'unused_credit'};
@@ -801,25 +809,13 @@ sub cancel {
     else {
       $do_credit = $self->part_pkg->option('unused_credit_cancel', 1);
     }
-    if ( $do_credit
-          and $last_bill > 0 # the package has been billed
-          and $next_bill > 0 # the package has a next bill date
-          and $next_bill >= $cancel_time # which is in the future
-    ) {
-      my $remaining_value = $self->calc_remain('time' => $cancel_time);
-      if ( $remaining_value > 0 ) {
-        my $error = $self->cust_main->credit(
-          $remaining_value,
-          'Credit for unused time on '. $self->part_pkg->pkg,
-          'reason_type' => $conf->config('cancel_credit_type'),
-        );
-        if ($error) {
-          $dbh->rollback if $oldAutoCommit;
-          return "Error crediting customer \$$remaining_value for unused time".
-                 " on ". $self->part_pkg->pkg. ": $error";
-        }
-      } #if $remaining_value
-    } #if $do_credit
+    if ( $do_credit ) {
+      my $error = $self->credit_remaining($cancel_time);
+      if ($error) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
 
   } #unless $date
 
@@ -978,6 +974,7 @@ sub suspend {
     return "";  # no error                     # complain on adjourn?
   }
 
+  my $suspend_time = $options{'time'} || time;
   my $date = $options{date} if $options{date}; # adjourn/suspend later
   $date = '' if ($date && $date <= time);      # complain instead?
 
@@ -986,7 +983,23 @@ sub suspend {
     return "Package $pkgnum expires before it would be suspended.";
   }
 
-  my $suspend_time = $options{'time'} || time;
+  # some false laziness with sub cancel
+  if ( !$options{nobill} && !$date &&
+       $self->part_pkg->option('bill_suspend_as_cancel',1) ) {
+    # kind of a kludge--'bill_suspend_as_cancel' to avoid having to 
+    # make the entire cust_main->bill path recognize 'suspend' and 
+    # 'cancel' separately.
+    warn "Billing $pkgnum on suspension (at $suspend_time)\n" if $DEBUG;
+    my $copy = $self->new({$self->hash});
+    my $error =
+      $copy->cust_main->bill( 'pkg_list' => [ $copy ], 
+                              'cancel'   => 1,
+                              'time'     => $suspend_time );
+    warn "Error billing during suspend, custnum ".
+      #$self->cust_main->custnum. ": $error"
+      ": $error"
+      if $error;
+  }
 
   if ( $options{'reason'} ) {
     $error = $self->insert_reason( 'reason' => $options{'reason'},
@@ -1014,6 +1027,14 @@ sub suspend {
   }
 
   unless ( $date ) {
+    # credit remaining time if appropriate
+    if ( $self->part_pkg->option('unused_credit_suspend', 1) ) {
+      my $error = $self->credit_remaining($suspend_time);
+      if ($error) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
 
     my @labels = ();
 
@@ -1073,6 +1094,34 @@ sub suspend {
   ''; #no errors
 }
 
+sub credit_remaining {
+  # Add a credit for remaining service
+  my $self = shift;
+  my $time = shift or die 'no suspend/cancel time';
+  my $conf = FS::Conf->new;
+  my $last_bill = $self->getfield('last_bill') || 0;
+  my $next_bill = $self->getfield('bill') || 0;
+  if ( $last_bill > 0         # the package has been billed
+      and $next_bill > 0      # the package has a next bill date
+      and $next_bill >= $time # which is in the future
+  ) {
+    my $remaining_value = $self->calc_remain('time' => $time);
+    if ( $remaining_value > 0 ) {
+      warn "Crediting for $remaining_value on package ".$self->pkgnum."\n"
+        if $DEBUG;
+      my $error = $self->cust_main->credit(
+        $remaining_value,
+        'Credit for unused time on '. $self->part_pkg->pkg,
+        'reason_type' => $conf->config('cancel_credit_type'),
+      ); # need 'suspend_credit_type'?
+      return "Error crediting customer \$$remaining_value for unused time".
+        " on ". $self->part_pkg->pkg. ": $error"
+        if $error;
+    } #if $remaining_value
+  } #if $last_bill, etc.
+  '';
+}
+
 =item unsuspend [ OPTION => VALUE ... ]
 
 Unsuspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
@@ -1333,23 +1382,23 @@ sub change {
   }
 
   my $unused_credit = 0;
-  if ( $opt->{'keep_dates'} ) {
-    foreach my $date ( qw(setup bill last_bill susp adjourn cancel expire 
-                          start_date contract_end ) ) {
-      $hash{$date} = $self->getfield($date);
-    }
-  }
+  my $keep_dates = $opt->{'keep_dates'};
   # Special case.  If the pkgpart is changing, and the customer is
   # going to be credited for remaining time, don't keep setup, bill, 
   # or last_bill dates, and DO pass the flag to cancel() to credit 
   # the customer.
-  if ( $opt->{'pkgpart'} 
-      and $opt->{'pkgpart'} != $self->pkgpart
-      and $self->part_pkg->option('unused_credit_change', 1) ) {
-    $unused_credit = 1;
+  if ( $opt->{'pkgpart'} and $opt->{'pkgpart'} != $self->pkgpart ) {
+    $keep_dates = 0;
+    $unused_credit = 1 if $self->part_pkg->option('unused_credit_change', 1);
     $hash{$_} = '' foreach qw(setup bill last_bill);
   }
 
+  if ( $keep_dates ) {
+    foreach my $date ( qw(setup bill last_bill susp adjourn cancel expire 
+                          start_date contract_end ) ) {
+      $hash{$date} = $self->getfield($date);
+    }
+  }
   # allow $opt->{'locationnum'} = '' to specifically set it to null
   # (i.e. customer default location)
   $opt->{'locationnum'} = $self->locationnum if !exists($opt->{'locationnum'});
@@ -1413,7 +1462,14 @@ sub change {
 
   #Good to go, cancel old package.  Notify 'cancel' of whether to credit 
   #remaining time.
-  $error = $self->cancel( quiet=>1, unused_credit => $unused_credit );
+  #Don't allow billing the package (preceding period packages and/or 
+  #outstanding usage) if we are keeping dates (i.e. location changing), 
+  #because the new package will be billed for the same date range.
+  $error = $self->cancel(
+    quiet         => 1, 
+    unused_credit => $unused_credit,
+    nobill        => $keep_dates
+  );
   if ($error) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
index ed5fa96..1c30b67 100644 (file)
@@ -1290,7 +1290,6 @@ sub calc_recur { die 'no calc_recur for '. shift->plan. "\n"; }
 
 #fallback that return 0 for old legacy packages with no plan
 sub calc_remain { 0; }
-sub calc_cancel { 0; }
 sub calc_units  { 0; }
 
 #fallback for everything except bulk.pm
@@ -1299,6 +1298,23 @@ sub hide_svc_detail { 0; }
 #fallback for packages that can't/won't summarize usage
 sub sum_usage { 0; }
 
+# somewhat more intelligent fallback--
+# covers the standard cases of billing outstanding usage or just running
+# another recurring billing cycle
+sub calc_cancel {
+  my $self = shift;
+  my $conf = new FS::Conf;
+  if ( $self->recur_temporality eq 'preceding'
+       and $self->option('bill_recur_on_cancel',1) ) {
+    return $self->calc_recur(@_);
+  }
+  elsif ( $conf->exists('bill_usage_on_cancel') # should be a package option?
+          and $self->can('calc_usage') ) {
+    return $self->calc_usage(@_);
+  }
+  0;
+}
+
 =item recur_cost_permonth CUST_PKG
 
 recur_cost divided by freq (only supported for monthly and longer frequencies)
index 0463b10..531a6a8 100644 (file)
@@ -70,7 +70,14 @@ tie my %contract_years, 'Tie::IxHash', (
                                     'unsuspending',
                           'type' => 'checkbox',
                         },
-
+    'bill_recur_on_cancel' => {
+                        'name' => 'Bill the last period on cancellation',
+                        'type' => 'checkbox',
+                        },
+    'bill_suspend_as_cancel' => {
+                        'name' => 'Bill immediately upon suspension', #desc?
+                        'type' => 'checkbox',
+                        },
     'externalid' => { 'name'   => 'Optional External ID',
                       'default' => '',
                     },
@@ -81,6 +88,8 @@ tie my %contract_years, 'Tie::IxHash', (
                         start_1st
                         sync_bill_date prorate_defer_bill prorate_round_day
                         suspend_bill unsuspend_adjust_bill
+                        bill_recur_on_cancel
+                        bill_suspend_as_cancel
                         externalid ),
                   ],
   'weight' => 10,
@@ -138,7 +147,7 @@ sub calc_recur {
   my $last_bill = $cust_pkg->get('last_bill'); #->last_bill falls back to setup
 
   return 0
-    if $self->recur_temporality eq 'preceding' && $last_bill == 0;
+    if $self->recur_temporality eq 'preceding' && !$last_bill;
 
   my $charge = $self->base_recur($cust_pkg, $sdate);
   if ( my $cutoff_day = $self->cutoff_day($cust_pkg) ) {
index 37891e2..11b7ee8 100644 (file)
@@ -52,12 +52,13 @@ sub calc_recur {
 
 sub can_discount { 0; }
 
-sub calc_cancel {
-  my $self = shift;
-  my($cust_pkg, $sdate, $details, $param ) = @_;
-
-  $self->calc_usage(@_);
-}
+# use the default
+#sub calc_cancel {
+#  my $self = shift;
+#  my($cust_pkg, $sdate, $details, $param ) = @_;
+#
+#  $self->calc_usage(@_);
+#}
 
 sub calc_usage {
   my $self = shift;
index e74ed0e..0072401 100644 (file)
@@ -315,12 +315,13 @@ sub calc_recur {
 
 }
 
-sub calc_cancel {
-  my $self = shift;
-  my($cust_pkg, $sdate, $details, $param ) = @_;
-
-  $self->calc_usage(@_);
-}
+# use the default
+#sub calc_cancel {
+#  my $self = shift;
+#  my($cust_pkg, $sdate, $details, $param ) = @_;
+#
+#  $self->calc_usage(@_);
+#}
 
 #false laziness w/voip_sqlradacct calc_recur resolve it if that one ever gets used again
 
index 7fb0a5d..a16ef1f 100644 (file)
@@ -177,12 +177,13 @@ sub calc_recur {
 
 }
 
-sub calc_cancel {
-  my $self = shift;
-  my($cust_pkg, $sdate, $details, $param ) = @_;
-
-  $self->calc_usage(@_);
-}
+# use the default
+#sub calc_cancel {
+#  my $self = shift;
+#  my($cust_pkg, $sdate, $details, $param ) = @_;
+#
+#  $self->calc_usage(@_);
+#}
 
 #false laziness w/voip_sqlradacct calc_recur resolve it if that one ever gets used again