future package unsuspend date, #14144
authormark <mark>
Sat, 28 Jan 2012 23:20:11 +0000 (23:20 +0000)
committermark <mark>
Sat, 28 Jan 2012 23:20:11 +0000 (23:20 +0000)
FS/FS/Cron/bill.pm
FS/FS/Schema.pm
FS/FS/cust_main/Billing.pm
FS/FS/cust_pkg.pm
httemplate/edit/REAL_cust_pkg.cgi
httemplate/edit/process/REAL_cust_pkg.cgi
httemplate/elements/change_history_common.html
httemplate/misc/cancel_pkg.html
httemplate/misc/process/cancel_pkg.html
httemplate/view/cust_main/packages/status.html

index 64979ba..8d1223b 100644 (file)
@@ -146,7 +146,7 @@ sub bill {
 #      (or now, if no -d switch was given).
 #
 #  -n: When used with "-d" and/or "-y", specifies that invoices should be dated
-#      with today's date, irregardless of the pretend date used to pre-generate
+#      with today's date, regardless of the pretend date used to pre-generate
 #      the invoices.
 #
 #  -p: Only process customers with the specified payby (I<CARD>, I<DCRD>, I<CHEK>, I<DCHK>, I<BILL>, I<COMP>, I<LECB>)
@@ -211,6 +211,7 @@ sub bill_where {
                 OR bill  IS NULL OR bill  <= $billtime 
                 OR ( expire  IS NOT NULL AND expire  <= $^T )
                 OR ( adjourn IS NOT NULL AND adjourn <= $^T )
+                OR ( resume  IS NOT NULL AND resume  <= $^T )
               )
     )
 END
index 6727420..9de1b7f 100644 (file)
@@ -1481,6 +1481,7 @@ sub tables_hashref {
         'last_bill',      @date_type,             '', '', 
         'susp',           @date_type,             '', '', 
         'adjourn',        @date_type,             '', '', 
+        'resume',         @date_type,             '', '', 
         'cancel',         @date_type,             '', '', 
         'expire',         @date_type,             '', '', 
         'contract_end',   @date_type,             '', '',
index 23d3b49..ed7b0fa 100644 (file)
@@ -129,6 +129,14 @@ sub bill_and_collect {
     else                                                     { warn   $error; }
   }
 
+  $error = $self->unsuspend_resumed_pkgs( day_end( $options{actual_time} ) );
+  if ( $error ) {
+    $error = "Error resuming custnum ".$self->custnum. ": $error";
+    if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
+    elsif ( $options{fatal}                                ) { die    $error; }
+    else                                                     { warn   $error; }
+  }
+
   $job->update_statustext('20,billing packages') if $job;
   $error = $self->bill( %options );
   if ( $error ) {
@@ -185,7 +193,7 @@ sub cancel_expired_pkgs {
     push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
   }
 
-  scalar(@errors) ? join(' / ', @errors) : '';
+  join(' / ', @errors);
 
 }
 
@@ -227,7 +235,25 @@ sub suspend_adjourned_pkgs {
     push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
   }
 
-  scalar(@errors) ? join(' / ', @errors) : '';
+  join(' / ', @errors);
+
+}
+
+sub unsuspend_resumed_pkgs {
+  my ( $self, $time, %options ) = @_;
+  
+  my @unsusp_pkgs = $self->ncancelled_pkgs( { 
+    'extra_sql' => " AND resume IS NOT NULL AND resume > 0 AND resume <= $time "
+  } );
+
+  my @errors = ();
+
+  foreach my $cust_pkg ( @unsusp_pkgs ) {
+    my $error = $cust_pkg->unsuspend( 'time' => $time );
+    push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
+  }
+
+  join(' / ', @errors);
 
 }
 
@@ -2168,6 +2194,7 @@ sub apply_payments {
 
     cancel_expired_pkgs
     suspend_adjourned_pkgs
+    unsuspend_resumed_pkgs
 
     bill
       (do_cust_event pre-bill)
index 469bd94..855accc 100644 (file)
@@ -605,6 +605,7 @@ sub check {
     || $self->ut_numbern('susp')
     || $self->ut_numbern('cancel')
     || $self->ut_numbern('adjourn')
+    || $self->ut_numbern('resume')
     || $self->ut_numbern('expire')
     || $self->ut_numbern('dundate')
     || $self->ut_enum('no_auto', [ '', 'Y' ])
@@ -618,6 +619,9 @@ sub check {
   return "A package with both start date (future start) and setup date (already started) will never bill"
     if $self->start_date && $self->setup;
 
+  return "A future unsuspend date can only be set for a package with a suspend date"
+    if $self->resume and !$self->susp and !$self->adjourn;
+
   $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
 
   if ( $self->dbdef_table->column('manual_flag') ) {
@@ -936,9 +940,21 @@ Available options are:
 
 =over 4
 
-=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 
+suspend (adjourn)
+
+=item time - can be set to override the current time, for calculation 
+of final invoices or unused-time credits
 
-=item date - can be set to a unix style timestamp to specify when to suspend (adjourn)
+=item resume_date - can be set to a time when the package should be 
+unsuspended.  This may be more convenient than calling C<unsuspend()>
+separately.
 
 =back
 
@@ -976,7 +992,7 @@ sub suspend {
 
   my $suspend_time = $options{'time'} || time;
   my $date = $options{date} if $options{date}; # adjourn/suspend later
-  $date = '' if ($date && $date <= time);      # complain instead?
+  $date = '' if ($date && $date <= $suspend_time); # complain instead?
 
   if ( $date && $old->get('expire') && $old->get('expire') < $date ) {
     dbh->rollback if $oldAutoCommit;
@@ -1019,6 +1035,12 @@ sub suspend {
   } else {
     $hash{'susp'} = $suspend_time;
   }
+
+  my $resume_date = $options{'resume_date'} || 0;
+  if ( $resume_date > ($date || $suspend_time) ) {
+    $hash{'resume'} = $resume_date;
+  }
+
   my $new = new FS::cust_pkg ( \%hash );
   $error = $new->replace( $self, options => { $self->options } );
   if ( $error ) {
@@ -1098,7 +1120,7 @@ sub suspend {
 
 Generate a credit for this package for the time remaining in the current 
 billing period.  MODE is either "suspend" or "cancel" (determines the 
-credit type).  TIME is the time of suspension/cancellation.  Both options
+credit type).  TIME is the time of suspension/cancellation.  Both arguments
 are mandatory.
 
 =cut
@@ -1146,6 +1168,11 @@ Available options are:
 
 =over 4
 
+=item date
+
+Can be set to a date to unsuspend the package in the future (the 'resume' 
+field).
+
 =item adjust_next_bill
 
 Can be set true to adjust the next bill date forward by
@@ -1180,15 +1207,38 @@ sub unsuspend {
 
   my $pkgnum = $old->pkgnum;
   if ( $old->get('cancel') || $self->get('cancel') ) {
-    dbh->rollback if $oldAutoCommit;
+    $dbh->rollback if $oldAutoCommit;
     return "Can't unsuspend cancelled package $pkgnum";
   }
 
   unless ( $old->get('susp') && $self->get('susp') ) {
-    dbh->rollback if $oldAutoCommit;
+    $dbh->rollback if $oldAutoCommit;
     return "";  # no error                     # complain instead?
   }
 
+  my $date = $opt{'date'};
+  if ( $date and $date > time ) { # return an error if $date <= time?
+
+    if ( $old->get('expire') && $old->get('expire') < $date ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Package $pkgnum expires before it would be unsuspended.";
+    }
+
+    my $new = new FS::cust_pkg { $self->hash };
+    $new->set('resume', $date);
+    $error = $new->replace($self, options => $self->options);
+
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+    else {
+      $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+      return '';
+    }
+  
+  } #if $date 
+
   foreach my $cust_svc (
     qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } )
   ) {
@@ -1229,7 +1279,8 @@ sub unsuspend {
   }
 
   $hash{'susp'} = '';
-  $hash{'adjourn'} = '' if $hash{'adjourn'} < time;
+  $hash{'adjourn'} = '' if $hash{'adjourn'} and $hash{'adjourn'} < time;
+  $hash{'resume'} = '' if !$hash{'adjourn'};
   my $new = new FS::cust_pkg ( \%hash );
   $error = $new->replace( $self, options => { $self->options } );
   if ( $error ) {
@@ -1287,6 +1338,7 @@ sub unadjourn {
 
   my %hash = $self->hash;
   $hash{'adjourn'} = '';
+  $hash{'resume'}  = '';
   my $new = new FS::cust_pkg ( \%hash );
   $error = $new->replace( $self, options => { $self->options } );
   if ( $error ) {
@@ -1409,7 +1461,7 @@ sub change {
 
   if ( $keep_dates ) {
     foreach my $date ( qw(setup bill last_bill susp adjourn cancel expire 
-                          start_date contract_end ) ) {
+                          resume start_date contract_end ) ) {
       $hash{$date} = $self->getfield($date);
     }
   }
index 170281b..0e18a52 100755 (executable)
@@ -61,6 +61,7 @@
 %#}
   <& .row_display, cust_pkg=>$cust_pkg, column=>'adjourn',  label=>'Adjournment', note=>'(will <b>suspend</b> this package when the date is reached)' &>
   <& .row_display, cust_pkg=>$cust_pkg, column=>'susp',     label=>'Suspension' &>
+  <& .row_display, cust_pkg=>$cust_pkg, column=>'resume',   label=>'Resumption', note=> '(will <b>unsuspend</b> this package when the date is reached' &>
 
   <& .row_display, cust_pkg=>$cust_pkg, column=>'expire',   label=>'Expiration', note=>'(will <b>cancel</b> this package when the date is reached)' &>
   <& .row_display, cust_pkg=>$cust_pkg, column=>'cancel',   label=>'Cancellation' &>
@@ -192,7 +193,7 @@ if ( $cgi->param('error') ) {
   $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
   die "No package!" unless $cust_pkg;
 
-  foreach my $col (qw( start_date setup last_bill bill adjourn expire )) {
+  foreach my $col (qw( start_date setup last_bill bill )) {
     my $value = $cgi->param($col);
     $cust_pkg->set( $col, $value ? parse_datetime($value) : '' );
   }
index 3a62ee0..9c36c8b 100755 (executable)
@@ -20,7 +20,8 @@ my $pkgnum = $cgi->param('pkgnum') or die;
 my $old = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
 my %hash = $old->hash;
 $hash{$_}= $cgi->param($_) ? parse_datetime($cgi->param($_)) : ''
-  foreach qw( start_date setup bill last_bill adjourn expire contract_end );
+  foreach qw( start_date setup bill last_bill contract_end );
+  # adjourn, expire, resume not editable this way
 
 my @errors = ();
 
index 9e19539..232664e 100644 (file)
@@ -138,7 +138,7 @@ my %action = (
 
 my %cust_pkg_date_fields = map { $_=>1 } qw(
   start_date setup bill last_bill susp adjourn cancel expire contract_end
-  change_date
+  resume change_date
 );
 
 # finding the other replace row
index 6e02e0e..4b5df86 100755 (executable)
@@ -1,10 +1,5 @@
 <& /elements/header-popup.html, mt($title) &>
 
-<LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
-<SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
-<SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
-<SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
-
 <& /elements/error.html &>
 
 <FORM NAME="sc_popup" ACTION="<% popurl(1) %>process/cancel_pkg.html" METHOD=POST>
 <% emt(ucfirst($method)." [_1]", $part_pkg->pkg_comment) %>
 <% ntable("#cccccc", 2) %>
 
-% if ($method eq 'expire' || $method eq 'adjourn') {
-<TR>
-% $submit =~ /^(\w*)\s/;
-  <TD><% mt("$1 package on") |h %> </TD>
-    <TD><INPUT TYPE="text" NAME="date" ID="expire_date" VALUE="<% $date |h %>">
-        <IMG SRC="<% $p %>images/calendar.png" ID="expire_button" STYLE="cursor:pointer" TITLE="<% mt('Select date') |h %>">
-        <BR><I><% mt('m/d/y') |h %></I>
-    </TD>
-</TR>
-<SCRIPT TYPE="text/javascript">
-  Calendar.setup({
-    inputField: "expire_date",
-    ifFormat:   "<% $date_format %>",
-    button:     "expire_button",
-    align:      "BR"
-  });
-</SCRIPT>
-%}
-%
-
+% my $date_init = 0;
+% if ($method eq 'expire' || $method eq 'adjourn' || $method eq 'resume') {
+%   $submit =~ /^(\w*)\s/;
+<& /elements/tr-input-date-field.html, {
+    'name'    => 'date',
+    'value'   => $date,
+    'label'   => mt("$1 package on"),
+    'format'  => $date_format,
+} &>
+%   $date_init = 1;
+% }
+
+% unless ( $method eq 'resume' ) { #the only one that doesn't need a reason
 <& /elements/tr-select-reason.html,
-             'field'          => 'reasonnum',
-             'reason_class'   => $class,
-             'curr_value'     => $reasonnum,
-             'control_button' => "document.getElementById('confirm_cancel_pkg_button')",
+     'field'          => 'reasonnum',
+     'reason_class'   => $class,
+     'curr_value'     => $reasonnum,
+     'control_button' => "document.getElementById('confirm_cancel_pkg_button')",
 &>
-
+% }
+
+% if ( ( $method eq 'adjourn' or $method eq 'suspend' ) and 
+%      $curuser->access_right('Unsuspend customer package') )  { #later?
+%   my $resume_date = $cgi->param('error') 
+%                     ? str2time($cgi->param('resume_date'))
+%                     : $cust_pkg->get('resume');
+
+<& /elements/tr-input-date-field.html, {
+    'name'    => 'resume_date',
+    'value'   => $resume_date,
+    'label'   => mt('Unsuspend on'),
+    'format'  => $date_format,
+    'noinit'  => $date_init,
+} &>
+% }
 </TABLE>
 
 <BR>
-<INPUT TYPE="submit" NAME="submit" ID="confirm_cancel_pkg_button" VALUE="<% mt($submit) |h %>" DISABLED>
+<INPUT TYPE="submit" NAME="submit" ID="confirm_cancel_pkg_button" 
+  VALUE="<% mt($submit) |h %>"
+  <% $method ne 'resume' ? 'DISABLED' : '' %>>
 
 </FORM>
 </BODY>
 my $conf = new FS::Conf;
 my $date_format = $conf->config('date_format') || '%m/%d/%Y';
 
-my $date = time2str($date_format, time);
+my $date;
 
 my($pkgnum, $reasonnum);
 if ( $cgi->param('error') ) {
   $pkgnum    = $cgi->param('pkgnum');
   $reasonnum = $cgi->param('reasonnum');
-  $date      = $cgi->param('date');
+  $date      = str2time($cgi->param('date'));
 } elsif ( $cgi->param('pkgnum') =~ /^(\d+)$/ ) {
   $pkgnum    = $1;
   $reasonnum = '';
@@ -90,6 +95,10 @@ if ($method eq 'cancel') {
   $class  = 'S';
   $submit = "Suspend Later";
   $right  = 'Suspend customer package later';
+} elsif ( $method eq 'resume') {
+  $class  = '';
+  $submit = 'Unsuspend Later';
+  $right  = 'Unsuspend customer package'; #later?
 } else {
   die 'illegal query (unknown method param)';
 }
@@ -104,4 +113,7 @@ my $cust_pkg = qsearchs('cust_pkg', {'pkgnum' => $pkgnum})
 
 my $part_pkg = $cust_pkg->part_pkg;
 
+$date ||= $cust_pkg->get($method);
+$date ||= time;
+
 </%init>
index a4371e6..662a776 100755 (executable)
@@ -1,4 +1,4 @@
-<% header(emt("Package $past{$method}")) %>
+<% header(emt("Package $past_method")) %>
   <SCRIPT TYPE="text/javascript">
     window.top.location.reload();
   </SCRIPT>
@@ -10,6 +10,7 @@ my %past = ( 'cancel'  => 'cancelled',
              'expire'  => 'expired',
              'suspend' => 'suspended',
              'adjourn' => 'adjourned',
+             'resume'  => 'scheduled to resume',
            );
 
 #i'm sure this is false laziness with somewhere, at least w/misc/cancel_pkg.html
@@ -17,6 +18,7 @@ my %right = ( 'cancel'  => 'Cancel customer package immediately',
               'expire'  => 'Cancel customer package later',
               'suspend' => 'Suspend customer package',
               'adjourn' => 'Suspend customer package later',
+              'resume'  => 'Unsuspend customer package', #later?
             );
 
 </%once>
@@ -24,8 +26,9 @@ my %right = ( 'cancel'  => 'Cancel customer package immediately',
 
 #untaint method
 my $method = $cgi->param('method');
-$method =~ /^(cancel|expire|suspend|adjourn)$/ or die "Illegal method";
+$method =~ /^(cancel|expire|suspend|adjourn|resume)$/ or die "Illegal method";
 $method = $1;
+my $past_method = $past{$method};
 
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right($right{$method});
@@ -35,30 +38,42 @@ my $pkgnum = $cgi->param('pkgnum');
 $pkgnum =~ /^(\d+)$/ or die "Illegal pkgnum";
 $pkgnum = $1;
 
-#untaint reasonnum
-my $reasonnum = $cgi->param('reasonnum');
-$reasonnum =~ /^(-?\d+)$/ or die "Illegal reasonnum";
-$reasonnum = $1;
-
 my $date = time;
-if ($method eq 'expire' || $method eq 'adjourn'){
+if ($method eq 'expire' || $method eq 'adjourn' || $method eq 'resume'){
   #untaint date
-  $date = $cgi->param('date');
+  $date = $cgi->param('date'); #huh?
   parse_datetime($cgi->param('date')) =~ /^(\d+)$/ or die "Illegal date";
   $date = $1;
-  $method = ($method eq 'expire') ? 'cancel' : 'suspend';
+  $method = 'cancel'    if $method eq 'expire';
+  $method = 'suspend'   if $method eq 'adjourn';
+  $method = 'unsuspend' if $method eq 'resume';
+}
+
+my $resume_date;
+if ( $method eq 'suspend' ) { #or 'adjourn'
+  $resume_date = parse_datetime($cgi->param('resume_date'))
+    if $cgi->param('resume_date');
 }
 
 my $cust_pkg = qsearchs( 'cust_pkg', {'pkgnum'=>$pkgnum} );
 
-if ($reasonnum == -1) {
-  $reasonnum = {
-    'typenum' => scalar( $cgi->param('newreasonnumT') ),
-    'reason'  => scalar( $cgi->param('newreasonnum' ) ),
-  };
+#untaint reasonnum
+my $reasonnum = $cgi->param('reasonnum');
+if ( $method ne 'unsuspend' ) { #i.e. 'resume'
+  $reasonnum =~ /^(-?\d+)$/ or die "Illegal reasonnum";
+  $reasonnum = $1;
+
+  if ($reasonnum == -1) {
+    $reasonnum = {
+      'typenum' => scalar( $cgi->param('newreasonnumT') ),
+      'reason'  => scalar( $cgi->param('newreasonnum' ) ),
+    };
+  }
 }
 
-my $error = $cust_pkg->$method( 'reason' => $reasonnum, 'date' => $date );
+my $error = $cust_pkg->$method( 'reason'      => $reasonnum,
+                                'date'        => $date,
+                                'resume_date' => $resume_date );
 
 if ($error) {
   $cgi->param('error', $error);
index a592549..28df9da 100644 (file)
@@ -61,6 +61,7 @@
 %   if ( $part_pkg->option('suspend_bill', 1) ) {
       <% pkg_status_row_if( $cust_pkg, emt('Next bill'), 'bill', %opt, curuser=>$curuser ) %>
 %   }
+    <% pkg_status_row_if( $cust_pkg, emt('Will resume'), 'resume', %opt, curuser=>$curuser ) %>
     <% pkg_status_row_if( $cust_pkg, emt('Expires'), 'expire', %opt, curuser=>$curuser ) %>
     <% pkg_status_row_if( $cust_pkg, emt('Contract ends'), 'contract_end', %opt ) %>
 
@@ -69,7 +70,8 @@
         <FONT SIZE=-1>
 %         if ( $curuser->access_right('Unsuspend customer package') ) { 
             (&nbsp;<% pkg_unsuspend_link($cust_pkg) %>&nbsp;)
-%         } 
+            (&nbsp;<% pkg_resume_link($cust_pkg) %>&nbsp;)
+%         }
 %         if ( $curuser->access_right('Cancel customer package immediately') ) {
             (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
 %         } 
       <% pkg_status_row_if($cust_pkg, emt('Will automatically suspend by'), 'autosuspend', %opt) %>
       <% pkg_status_row_if($cust_pkg, emt('Automatic suspension delayed until'), 'dundate', %opt) %>
       <% pkg_status_row_if( $cust_pkg, emt('Will suspend on'), 'adjourn', %opt, curuser=>$curuser ) %>
+      <% pkg_status_row_if( $cust_pkg, emt('Will resume on'), 'resume', %opt, curuser=>$curuser ) %>
       <% pkg_status_row_if( $cust_pkg, emt('Expires'), 'expire', %opt, curuser=>$curuser ) %>
       <% pkg_status_row_if( $cust_pkg, emt('Contract ends'), 'contract_end', %opt ) %>
 
@@ -440,7 +443,17 @@ sub pkg_delay_link  {
          )
 }
 
-sub pkg_unsuspend_link { pkg_link('misc/unsusp_pkg',    emt('Unsuspend'), @_ ); }
+sub pkg_resume_link {
+  include( '/elements/popup_link-cust_pkg.html',
+             'action'      => $p. 'misc/cancel_pkg.html?method=resume',
+             'label'       => emt('Unsuspend later'),
+             'actionlabel' => emt('Resume'),
+             'color'       => '#00CC00',
+             'cust_pkg'    => shift,
+         )
+}
+
+sub pkg_unsuspend_link { pkg_link('misc/unsusp_pkg',    emt('Unsuspend now'), @_ ); }
 sub pkg_unadjourn_link { pkg_link('misc/unadjourn_pkg', emt('Abort'),     @_ ); }
 sub pkg_unexpire_link  { pkg_link('misc/unexpire_pkg',  emt('Abort'),     @_ ); }