future package change, #20687
authorMark Wells <mark@freeside.biz>
Tue, 16 Jul 2013 01:37:26 +0000 (18:37 -0700)
committerMark Wells <mark@freeside.biz>
Tue, 16 Jul 2013 01:37:26 +0000 (18:37 -0700)
FS/FS/Schema.pm
FS/FS/cust_main/Billing.pm
FS/FS/cust_pkg.pm
httemplate/edit/process/change-cust_pkg.html
httemplate/elements/tr-select-cust-part_pkg.html
httemplate/misc/change_pkg.cgi
httemplate/view/cust_main/packages.html
httemplate/view/cust_main/packages/location.html
httemplate/view/cust_main/packages/package.html
httemplate/view/cust_main/packages/section.html
httemplate/view/cust_main/packages/status.html

index 5e2e2ef..8d234de 100644 (file)
@@ -1798,6 +1798,7 @@ sub tables_hashref {
         'waive_setup',        'char', 'NULL',  1, '', '', 
         'recur_show_zero',    'char', 'NULL',  1, '', '',
         'setup_show_zero',    'char', 'NULL',  1, '', '',
+        'change_to_pkgnum',    'int', 'NULL', '', '', '',
       ],
       'primary_key' => 'pkgnum',
       'unique' => [],
index 220f66a..081dd70 100644 (file)
@@ -192,14 +192,30 @@ sub cancel_expired_pkgs {
 
   my @errors = ();
 
-  foreach my $cust_pkg ( @cancel_pkgs ) {
+  CUST_PKG: foreach my $cust_pkg ( @cancel_pkgs ) {
     my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
-    my $error = $cust_pkg->cancel($cpr ? ( 'reason'        => $cpr->reasonnum,
+    my $error;
+
+    if ( $cust_pkg->change_to_pkgnum ) {
+
+      my $new_pkg = FS::cust_pkg->by_key($cust_pkg->change_to_pkgnum);
+      if ( !$new_pkg ) {
+        push @errors, 'can\'t change pkgnum '.$cust_pkg->pkgnum.' to pkgnum '.
+                      $cust_pkg->change_to_pkgnum.'; not expiring';
+        next CUST_PKG;
+      }
+      $error = $cust_pkg->change( 'cust_pkg'        => $new_pkg,
+                                  'unprotect_svcs'  => 1 );
+      $error = '' if ref $error eq 'FS::cust_pkg';
+
+    } else { # just cancel it
+       $error = $cust_pkg->cancel($cpr ? ( 'reason'        => $cpr->reasonnum,
                                            'reason_otaker' => $cpr->otaker,
                                            'time'          => $time,
                                          )
                                        : ()
                                  );
+    }
     push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
   }
 
index dd67d03..5cac458 100644 (file)
@@ -210,6 +210,11 @@ The pkgnum of the package that this package is supplemental to, if any.
 The package link (L<FS::part_pkg_link>) that defines this supplemental
 package, if it is one.
 
+=item change_to_pkgnum
+
+The pkgnum of the package this one will be "changed to" in the future
+(on its expiration date).
+
 =back
 
 Note: setup, last_bill, bill, adjourn, susp, expire, cancel and change_date
@@ -353,15 +358,6 @@ sub insert {
     }
   }
 
-  #if ( $self->reg_code ) {
-  #  my $reg_code = qsearchs('reg_code', { 'code' => $self->reg_code } );
-  #  $error = $reg_code->delete;
-  #  if ( $error ) {
-  #    $dbh->rollback if $oldAutoCommit;
-  #    return $error;
-  #  }
-  #}
-
   my $conf = new FS::Conf;
 
   if ( $conf->config('ticket_system') && $options{ticket_subject} ) {
@@ -651,6 +647,7 @@ sub check {
     || $self->ut_enum('setup_show_zero', [ '', 'Y', 'N', ])
     || $self->ut_foreign_keyn('main_pkgnum', 'cust_pkg', 'pkgnum')
     || $self->ut_foreign_keyn('pkglinknum', 'part_pkg_link', 'pkglinknum')
+    || $self->ut_foreign_keyn('change_to_pkgnum', 'cust_pkg', 'pkgnum')
   ;
   return $error if $error;
 
@@ -872,10 +869,19 @@ sub cancel {
   } #unless $date
 
   my %hash = $self->hash;
-  $date ? ($hash{'expire'} = $date) : ($hash{'cancel'} = $cancel_time);
+  if ( $date ) {
+    $hash{'expire'} = $date;
+  } else {
+    $hash{'cancel'} = $cancel_time;
+  }
   $hash{'change_custnum'} = $options{'change_custnum'};
+
   my $new = new FS::cust_pkg ( \%hash );
   $error = $new->replace( $self, options => { $self->options } );
+  if ( $self->change_to_pkgnum ) {
+    my $change_to = FS::cust_pkg->by_key($self->change_to_pkgnum);
+    $error ||= $change_to->cancel || $change_to->delete;
+  }
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -1728,15 +1734,27 @@ New pkgpart (see L<FS::part_pkg>).
 
 New refnum (see L<FS::part_referral>).
 
+=item cust_pkg
+
+"New" (existing) FS::cust_pkg object.  The package's services and other 
+attributes will be transferred to this package.
+
 =item keep_dates
 
 Set to true to transfer billing dates (start_date, setup, last_bill, bill, 
 susp, adjourn, cancel, expire, and contract_end) to the new package.
 
+=item unprotect_svcs
+
+Normally, change() will rollback and return an error if some services 
+can't be transferred (also see the I<cust_pkg-change_svcpart> config option).
+If unprotect_svcs is true, this method will transfer as many services as 
+it can and then unconditionally cancel the old package.
+
 =back
 
-At least one of locationnum, cust_location, pkgpart, refnum must be specified 
-(otherwise, what's the point?)
+At least one of locationnum, cust_location, pkgpart, refnum, cust_main, or
+cust_pkg must be specified (otherwise, what's the point?)
 
 Returns either the new FS::cust_pkg object or a scalar error.
 
@@ -1793,6 +1811,12 @@ sub change {
     $opt->{'locationnum'} = $opt->{'cust_location'}->locationnum;
   }
 
+  if ( $opt->{'cust_pkg'} ) {
+    # treat changing to a package with a different pkgpart as a 
+    # pkgpart change (because it is)
+    $opt->{'pkgpart'} = $opt->{'cust_pkg'}->pkgpart;
+  }
+
   # whether to override pkgpart checking on the new package
   my $same_pkgpart = 1;
   if ( $opt->{'pkgpart'} and ( $opt->{'pkgpart'} != $self->pkgpart ) ) {
@@ -1844,16 +1868,30 @@ sub change {
 
   $hash{'contactnum'} = $opt->{'contactnum'} if $opt->{'contactnum'};
 
-  # Create the new package.
-  my $cust_pkg = new FS::cust_pkg {
-    custnum        => $custnum,
-    pkgpart        => ( $opt->{'pkgpart'}     || $self->pkgpart      ),
-    refnum         => ( $opt->{'refnum'}      || $self->refnum       ),
-    locationnum    => ( $opt->{'locationnum'}                        ),
-    %hash,
-  };
-  $error = $cust_pkg->insert( 'change' => 1,
-                              'allow_pkgpart' => $same_pkgpart );
+  my $cust_pkg;
+  if ( $opt->{'cust_pkg'} ) {
+    # The target package already exists; update it to show that it was 
+    # changed from this package.
+    $cust_pkg = $opt->{'cust_pkg'};
+
+    foreach ( qw( pkgnum pkgpart locationnum ) ) {
+      $cust_pkg->set("change_$_", $self->get($_));
+    }
+    $cust_pkg->set('change_date', $time);
+    $error = $cust_pkg->replace;
+
+  } else {
+    # Create the new package.
+    $cust_pkg = new FS::cust_pkg {
+      custnum        => $custnum,
+      pkgpart        => ( $opt->{'pkgpart'}     || $self->pkgpart      ),
+      refnum         => ( $opt->{'refnum'}      || $self->refnum       ),
+      locationnum    => ( $opt->{'locationnum'}                        ),
+      %hash,
+    };
+    $error = $cust_pkg->insert( 'change' => 1,
+                                'allow_pkgpart' => $same_pkgpart );
+  }
   if ($error) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -1878,7 +1916,11 @@ sub change {
     }
   }
 
-  if ($error > 0) {
+  # We set unprotect_svcs when executing a "future package change".  It's 
+  # not a user-interactive operation, so returning an error means the 
+  # package change will just fail.  Rather than have that happen, we'll 
+  # let leftover services be deleted.
+  if ($error > 0 and !$opt->{'unprotect_svcs'}) {
     # Transfers were successful, but we still had services left on the old
     # package.  We can't change the package under this circumstances, so abort.
     $dbh->rollback if $oldAutoCommit;
@@ -1939,57 +1981,62 @@ sub change {
       return "Error transferring package notes: $error";
     }
   }
-
-  # Order any supplemental packages.
-  my $part_pkg = $cust_pkg->part_pkg;
-  my @old_supp_pkgs = $self->supplemental_pkgs;
+  
   my @new_supp_pkgs;
-  foreach my $link ($part_pkg->supp_part_pkg_link) {
-    my $old;
-    foreach (@old_supp_pkgs) {
-      if ($_->pkgpart == $link->dst_pkgpart) {
-        $old = $_;
-        $_->pkgpart(0); # so that it can't match more than once
+
+  if ( !$opt->{'cust_pkg'} ) {
+    # Order any supplemental packages.
+    my $part_pkg = $cust_pkg->part_pkg;
+    my @old_supp_pkgs = $self->supplemental_pkgs;
+    foreach my $link ($part_pkg->supp_part_pkg_link) {
+      my $old;
+      foreach (@old_supp_pkgs) {
+        if ($_->pkgpart == $link->dst_pkgpart) {
+          $old = $_;
+          $_->pkgpart(0); # so that it can't match more than once
+        }
+        last if $old;
       }
-      last if $old;
-    }
-    # false laziness with FS::cust_main::Packages::order_pkg
-    my $new = FS::cust_pkg->new({
-        pkgpart       => $link->dst_pkgpart,
-        pkglinknum    => $link->pkglinknum,
-        custnum       => $custnum,
-        main_pkgnum   => $cust_pkg->pkgnum,
-        locationnum   => $cust_pkg->locationnum,
-        start_date    => $cust_pkg->start_date,
-        order_date    => $cust_pkg->order_date,
-        expire        => $cust_pkg->expire,
-        adjourn       => $cust_pkg->adjourn,
-        contract_end  => $cust_pkg->contract_end,
-        refnum        => $cust_pkg->refnum,
-        discountnum   => $cust_pkg->discountnum,
-        waive_setup   => $cust_pkg->waive_setup,
-    });
-    if ( $old and $opt->{'keep_dates'} ) {
-      foreach (qw(setup bill last_bill)) {
-        $new->set($_, $old->get($_));
+      # false laziness with FS::cust_main::Packages::order_pkg
+      my $new = FS::cust_pkg->new({
+          pkgpart       => $link->dst_pkgpart,
+          pkglinknum    => $link->pkglinknum,
+          custnum       => $custnum,
+          main_pkgnum   => $cust_pkg->pkgnum,
+          locationnum   => $cust_pkg->locationnum,
+          start_date    => $cust_pkg->start_date,
+          order_date    => $cust_pkg->order_date,
+          expire        => $cust_pkg->expire,
+          adjourn       => $cust_pkg->adjourn,
+          contract_end  => $cust_pkg->contract_end,
+          refnum        => $cust_pkg->refnum,
+          discountnum   => $cust_pkg->discountnum,
+          waive_setup   => $cust_pkg->waive_setup,
+      });
+      if ( $old and $opt->{'keep_dates'} ) {
+        foreach (qw(setup bill last_bill)) {
+          $new->set($_, $old->get($_));
+        }
       }
+      $error = $new->insert( allow_pkgpart => $same_pkgpart );
+      # transfer services
+      if ( $old ) {
+        $error ||= $old->transfer($new);
+      }
+      if ( $error and $error > 0 ) {
+        # no reason why this should ever fail, but still...
+        $error = "Unable to transfer all services from supplemental package ".
+          $old->pkgnum;
+      }
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+      push @new_supp_pkgs, $new;
     }
-    $error = $new->insert( allow_pkgpart => $same_pkgpart );
-    # transfer services
-    if ( $old ) {
-      $error ||= $old->transfer($new);
-    }
-    if ( $error and $error > 0 ) {
-      # no reason why this should ever fail, but still...
-      $error = "Unable to transfer all services from supplemental package ".
-        $old->pkgnum;
-    }
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return $error;
-    }
-    push @new_supp_pkgs, $new;
-  }
+  } # if !$opt->{'cust_pkg'}
+    # because if there is one, then supplemental packages would already
+    # have been created for it.
 
   #Good to go, cancel old package.  Notify 'cancel' of whether to credit 
   #remaining time.
@@ -1997,6 +2044,11 @@ sub change {
   #outstanding usage) if we are keeping dates (i.e. location changing), 
   #because the new package will be billed for the same date range.
   #Supplemental packages are also canceled here.
+
+  # during scheduled changes, avoid canceling the package we just
+  # changed to (duh)
+  $self->set('change_to_pkgnum' => '');
+
   $error = $self->cancel(
     quiet          => 1, 
     unused_credit  => $unused_credit,
@@ -2025,6 +2077,141 @@ sub change {
 
 }
 
+=item change_later OPTION => VALUE...
+
+Schedule a package change for a later date.  This actually orders the new
+package immediately, but sets its start date for a future date, and sets
+the current package to expire on the same date.
+
+If the package is already scheduled for a change, this can be called with 
+'start_date' to change the scheduled date, or with pkgpart and/or 
+locationnum to modify the package change.  To cancel the scheduled change 
+entirely, see C<abort_change>.
+
+Options include:
+
+=over 4
+
+=item start_date
+
+The date for the package change.  Required, and must be in the future.
+
+=item pkgpart
+
+=item locationnum
+
+The pkgpart and locationnum of the new package, with the same 
+meaning as in C<change>.
+
+=back
+
+=cut
+
+sub change_later {
+  my $self = shift;
+  my $opt = ref($_[0]) ? shift : { @_ };
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $cust_main = $self->cust_main;
+
+  my $date = delete $opt->{'start_date'} or return 'start_date required';
+  if ( $date <= time ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "start_date $date is in the past";
+  }
+
+  my $error;
+
+  if ( $self->change_to_pkgnum ) {
+    my $change_to = FS::cust_pkg->by_key($self->change_to_pkgnum);
+    my $new_pkgpart = $opt->{'pkgpart'}
+        if $opt->{'pkgpart'} and $opt->{'pkgpart'} != $change_to->pkgpart;
+    my $new_locationnum = $opt->{'locationnum'}
+        if $opt->{'locationnum'} and $opt->{'locationnum'} != $change_to->locationnum;
+    if ( $new_pkgpart or $new_locationnum ) {
+      # it hasn't been billed yet, so in principle we could just edit
+      # it in place (w/o a package change), but that's bad form.
+      # So change the package according to the new options...
+      my $err_or_pkg = $change_to->change(%$opt);
+      if ( ref $err_or_pkg ) {
+        # Then set that package up for a future start.
+        $self->set('change_to_pkgnum', $err_or_pkg->pkgnum);
+        $self->set('expire', $date); # in case it's different
+        $err_or_pkg->set('start_date', $date);
+
+        $error = $self->replace       ||
+                 $err_or_pkg->replace ||
+                 $err_or_pkg->delete;
+      } else {
+        $error = $err_or_pkg;
+      }
+    } else { # change the start date only.
+      $self->set('expire', $date);
+      $change_to->set('start_date', $date);
+      $error = $self->replace || $change_to->replace;
+    }
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    } else {
+      $dbh->commit if $oldAutoCommit;
+      return '';
+    }
+  } # if $self->change_to_pkgnum
+
+  my $new_pkgpart = $opt->{'pkgpart'}
+      if $opt->{'pkgpart'} and $opt->{'pkgpart'} != $self->pkgpart;
+  my $new_locationnum = $opt->{'locationnum'}
+      if $opt->{'locationnum'} and $opt->{'locationnum'} != $self->locationnum;
+  return '' unless $new_pkgpart or $new_locationnum; # wouldn't do anything
+
+  my %hash = (
+    'custnum'     => $self->custnum,
+    'pkgpart'     => ($opt->{'pkgpart'}     || $self->pkgpart),
+    'locationnum' => ($opt->{'locationnum'} || $self->locationnum),
+    'start_date'  => $date,
+  );
+  my $new = FS::cust_pkg->new(\%hash);
+  $error = $new->insert('change' => 1, 
+                        'allow_pkgpart' => ($new_pkgpart ? 0 : 1));
+  if ( !$error ) {
+    $self->set('change_to_pkgnum', $new->pkgnum);
+    $self->set('expire', $date);
+    $error = $self->replace;
+  }
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+  } else {
+    $dbh->commit if $oldAutoCommit;
+  }
+
+  $error;
+}
+
+=item abort_change
+
+Cancels a future package change scheduled by C<change_later>.
+
+=cut
+
+sub abort_change {
+  my $self = shift;
+  my $pkgnum = $self->change_to_pkgnum;
+  my $change_to = FS::cust_pkg->by_key($pkgnum) if $pkgnum;
+  my $error;
+  if ( $change_to ) {
+    $error = $change_to->cancel || $change_to->delete;
+    return $error if $error;
+  }
+  $self->set('change_to_pkgnum', '');
+  $self->set('expire', '');
+  $self->replace;
+}
+
 =item set_quantity QUANTITY
 
 Change the package's quantity field.  This is the one package property
index c893f13..9d06d8e 100644 (file)
@@ -40,8 +40,35 @@ if ( $cgi->param('locationnum') == -1 ) {
   $change{'cust_location'} = $cust_location;
 }
 
-my $pkg_or_error = $cust_pkg->change( \%change );
+my $error;
+if ( $cgi->param('delay') ) {
+  my $date = parse_datetime($cgi->param('start_date'));
+  if (!$date) {
+    $error = "Invalid change date '".$cgi->param('start_date')."'.";
+  } elsif ( $date < time ) {
+    $error = "Change date ".$cgi->param('start_date')." is in the past.";
+  } else {
+    # schedule the change
+    $change{'start_date'} = $date;
+    $error = $cust_pkg->change_later(\%change);
+  }
+} else {
+  # special case: if there's a package change scheduled, and it matches
+  # the parameters the user requested this time, then change to the existing
+  # future package.
+  if ( $cust_pkg->change_to_pkgnum ) {
+    my $change_to = FS::cust_pkg->by_key($cust_pkg->change_to_pkgnum);
+    if ( $change_to->pkgpart      == $change{'pkgpart'} and
+         $change_to->locationnum  == $change{'locationnum'} ) {
 
-my $error = ref($pkg_or_error) ? '' : $pkg_or_error;
+      %change = ( 'cust_pkg' => $change_to );
+
+    }
+  }
+    
+  # do a package change right now
+  my $pkg_or_error = $cust_pkg->change( \%change );
+  $error = ref($pkg_or_error) ? '' : $pkg_or_error;
+}
 
 </%init>
index c9c50d2..488f04a 100644 (file)
@@ -20,7 +20,7 @@
 
       what.form.pkgpart.disabled = 'disabled'; //disable part_pkg dropdown
       var submitButton = what.form.submitButton; // || what.form.submit;
-      if ( submitButton ) {
+      if ( submitButton && <% $opt{'curr_value'} ? 0 : 1 %> ) {
         submitButton.disabled = true; //disable the submit button
       }
       var discountnum = what.form.discountnum;
@@ -51,6 +51,9 @@
         }
 
         what.form.pkgpart.disabled = ''; //re-enable part_pkg dropdown
+%       if ( $opt{'curr_value'} ) {
+        what.form.pkgpart.value = <% $opt{'curr_value'} %>;
+%       }
 
       }
 
index 03e336c..7425fbf 100755 (executable)
@@ -1,7 +1,6 @@
-<& /elements/header-popup.html, mt("Change Package") &>
+<& /elements/header-popup.html, mt($title) &>
 
 <SCRIPT TYPE="text/javascript" SRC="../elements/order_pkg.js"></SCRIPT>
-
 <& /elements/error.html &>
 
 <FORM NAME="OrderPkgForm" ACTION="<% $p %>edit/process/change-cust_pkg.html" METHOD=POST>
 
 </TABLE>
 
+<TABLE>
+  <TR>
+    <TD> Apply this change: </TD>
+    <TD> <INPUT TYPE="radio" NAME="delay" VALUE="0" \
+          <% !$cgi->param('delay') ? 'CHECKED' : '' %>> now </TD>
+    <TD> <INPUT TYPE="radio" NAME="delay" VALUE="1" \
+          <% $cgi->param('delay')  ? 'CHECKED' : '' %>> in the future
+      <& /elements/input-date-field.html, {
+  'name'  => 'start_date',
+  'value' => ($cgi->param('start_date') || $cust_main->next_bill_date),
+      } &>
+    </TD>
+  </TR>
+</TABLE>
+
 <& /elements/standardize_locations.html,
             'form'       => "OrderPkgForm",
             'callback'   => 'document.OrderPkgForm.submit();',
@@ -74,4 +88,15 @@ my $cust_main = $cust_pkg->cust_main
 
 my $part_pkg = $cust_pkg->part_pkg;
 
+my $title = "Change Package";
+
+# if there's already a package change ordered, preload it
+if ( $cust_pkg->change_to_pkgnum ) {
+  my $change_to = FS::cust_pkg->by_key($cust_pkg->change_to_pkgnum);
+  $cgi->param('delay', 1);
+  foreach(qw( start_date pkgpart locationnum )) {
+    $cgi->param($_, $change_to->get($_));
+  }
+  $title = "Edit Scheduled Package Change";
+}
 </%init>
index e32fe4c..566ab29 100755 (executable)
@@ -3,7 +3,6 @@ td.package {
   vertical-align: top;
   border-width: 0;
   border-style: solid;
-  border-color: #bbbbff;
 }
 table.package {
   border: none;
@@ -199,11 +198,30 @@ sub get_packages {
   } );
   my $num_old_packages = scalar(@packages);
 
+  my %change_to_from; # target pkgnum => current cust_pkg, for future changes
+
   foreach my $cust_pkg ( @packages ) {
     my %hash = $cust_pkg->hash;
     my %part_pkg = map  { /^part_pkg_(.+)$/ or die; ( $1 => $hash{$_} ); }
                    grep { /^part_pkg_/ } keys %hash;
     $cust_pkg->{'_pkgpart'} = new FS::part_pkg \%part_pkg;
+    if ( $cust_pkg->change_to_pkgnum ) {
+      $change_to_from{$cust_pkg->change_to_pkgnum} = $cust_pkg;
+    }
+  }
+
+  if ( keys %change_to_from ) {
+    my @not_future_packages;
+    foreach my $cust_pkg (@packages) {
+      if ( exists( $change_to_from{$cust_pkg->pkgnum} ) ) {
+        my $change_from = $change_to_from{ $cust_pkg->pkgnum };
+        $cust_pkg->set('change_from_pkg', $change_from);
+        $change_from->set('change_to_pkg', $cust_pkg);
+      } else {
+        push @not_future_packages, $cust_pkg;
+      }
+    }
+    @packages = @not_future_packages;
   }
 
   unless ( $cgi->param('showoldpackages') ) {
@@ -225,6 +243,7 @@ sub get_packages {
   
   # don't include supplemental packages in this list; they'll be found from
   # their main packages
+  # (as will change-target packages)
   @packages = grep !$_->main_pkgnum, @packages;
 
   ( \@packages, $num_old_packages );
index ab961b7..01cbc0f 100644 (file)
@@ -1,6 +1,11 @@
-% if ( $default ) {
-  <DIV STYLE="font-style: italic; font-size: small">
-% }
+% if ( $cust_pkg->change_from_pkg
+%      and $cust_pkg->change_from_pkg->locationnum == $cust_pkg->locationnum )
+% {
+% # don't show the location
+% } else {
+%   if ( $default ) {
+    <DIV STYLE="font-style: italic; font-size: small">
+%   }
 
     <% $loc->location_label( 'join_string'     => '<BR>',
                              'double_space'    => ' &nbsp; ',
         </FONT>
 %   }
 
-% if ( $default ) {
-  </DIV>
-% }
+%   if ( $default ) {
+    </DIV>
+%   }
 
-% if ( ! $cust_pkg->get('cancel')
+%   if ( ! $cust_pkg->get('cancel')
 %      && $FS::CurrentUser::CurrentUser->access_right('Change customer package')
-%    )
-% {
+%     )
+%   {
   <BR>
   <FONT SIZE=-1>
-%   unless ( $opt{no_links} ) {
+%     unless ( $opt{no_links} or $opt{'change_from'} ) {
       (&nbsp;<%pkg_change_location_link($cust_pkg)%>&nbsp;)
-%   }
-%   if ( $cust_pkg->locationnum && ! $opt{no_links} ) {
+%     }
+%     if ( $cust_pkg->locationnum && ! $opt{no_links} ) {
         (&nbsp;<%edit_location_link($cust_pkg->locationnum)%>&nbsp;)
-%   }
+%     }
   </FONT>
-% } 
-
+%   
+% }
 <%init>
 
 my $conf = new FS::Conf;
index 7aad9a4..596a473 100644 (file)
@@ -1,5 +1,4 @@
-<TD CLASS="inv package" BGCOLOR="<% $bgcolor %>" VALIGN="top"
-  STYLE="border-left-width: <% $supplemental * 30 %>px">
+<TD CLASS="inv package" BGCOLOR="<% $bgcolor %>" VALIGN="top" <%$style%>>
   <TABLE CLASS="inv package"> 
     <TR>
       <TD COLSPAN=2>
 
 %         unless ( $cust_pkg->get('cancel') || $opt{no_links} ) {
 %
-%           if ( $supplemental or $part_pkg->freq eq '0' ) {
+%           if ( $change_from ) {
+%             # This is the target package for a future change.
+%             # Nothing you can do with it besides modify/cancel the 
+%             # future change, and that's on the current package.
+%           } elsif ( $supplemental or $part_pkg->freq eq '0' ) {
 %             # Supplemental packages can't be changed independently.
 %             # One-time charges don't need to be changed.
 %             # For both of those, we only show "Add comments",
 %        )
 %     {
       <TR>
+% # yeah, I guess we'll let you do this on a future change package
 %       if ( FS::Conf->new->exists('invoice-unitprice') ) {
         <TD><FONT SIZE="-1">
           (&nbsp;<% pkg_change_quantity_link($cust_pkg) %>&nbsp;)
@@ -233,7 +237,21 @@ my $countrydefault = $opt{'countrydefault'} || 'US';
 my $statedefault   = $opt{'statedefault'}
                      || ($countrydefault eq 'US' ? 'CA' : '');
 
+# put a marker on the left edge of this column
+# if this package is somehow special
 my $supplemental = $opt{'supplemental'} || 0;
+my $change_from = $opt{'change_from'} || 0;
+my $style = '';
+if ( $supplemental or $change_from ) {
+  $style = 'border-left-width: '.($supplemental + $change_from)*30 . 'px; '.
+           'border-color: ';
+  if ( $supplemental ) {
+    $style .= '#bbbbff';
+  } elsif ( $change_from ) {
+    $style .= '#bbffbb';
+  }
+  $style = qq!STYLE="$style"!;
+}
 
 $cust_pkg->pkgnum =~ /^(\d+)$/;
 my $pkgnum = $1;
@@ -263,7 +281,7 @@ sub pkg_change_link {
     'actionlabel' => emt('Change'),
     'cust_pkg'    => $cust_pkg,
     'width'       => 763,
-    'height'      => 380,
+    'height'      => 480,
   );
 }
 
index 82d0620..0383fe8 100755 (executable)
     <& services.html, %iopt &>
   </TR>
 % $row++;
+% # show the change target, if there is one
+% if ( $cust_pkg->change_to_pkg ) {
+    <& .packagerow, $cust_pkg->change_to_pkg, %iopt, 'change_from' => 1 &>
+% }
 % # include supplemental packages if any
 % $iopt{'supplemental'} = ($iopt{'supplemental'} || 0) + 1;
 % foreach my $supp_pkg ($cust_pkg->supplemental_pkgs) {
index ed360cc..6894a4e 100644 (file)
       <% 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_expire($cust_pkg, %opt, curuser=>$curuser) %>
     <% pkg_status_row_if( $cust_pkg, emt('Contract ends'), 'contract_end', %opt ) %>
 
-% if ( !$supplemental && ! $opt{no_links} ) {
+% if ( !$supplemental && ! $opt{no_links} && !$change_from ) {
       <TR>
         <TD COLSPAN=<%$opt{colspan}%>>
           <FONT SIZE=-1>
+%           if ( $cust_pkg->change_to_pkgnum ) {
+%               # then you can modify the package change
+%               if ( $curuser->access_right('Change customer package') ) {
+                (&nbsp;<% pkg_change_now_link($cust_pkg) %>&nbsp;)
+                (&nbsp;<% pkg_change_later_link($cust_pkg) %>&nbsp;)
+                (&nbsp;<% pkg_unchange_link($cust_pkg) %>&nbsp;)
+                <BR>
+%               }
+%           }
 %           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') ) {
+%           if ( !$cust_pkg->change_to_pkgnum and
+%                $curuser->access_right('Cancel customer package immediately')
+%           ) {
               (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
 %           } 
           </FONT>
 %
 %   } else { #status: active
 %
-%     unless ( $cust_pkg->get('setup') ) { #not setup
+%     if ( $change_from ) { # future change
+%
+          <% pkg_status_row_colspan( $cust_pkg, emt('Waiting for package change'), '', %opt ) %>
+          <% pkg_status_row( $cust_pkg,
+                             emt('Will be activated on'),
+                             'start_date',
+                             %opt ) %>
 %
-%       unless ( $part_pkg->freq ) {
+%     } elsif ( ! $cust_pkg->get('setup') ) { # not setup
+%
+%       unless ( $part_pkg->freq ) { # one-time charge
 
           <% pkg_status_row_colspan( $cust_pkg, emt('Not yet billed (one-time charge)'), '', %opt ) %>
 
 
 %       } 
 %
-%     } 
+%     }
 %
 %     if ( $opt{'cust_pkg-show_autosuspend'} ) {
 %       my $autosuspend = pkg_autosuspend_time( $cust_pkg );
       <% 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_expire($cust_pkg, %opt, curuser=>$curuser) %>
       <% pkg_status_row_if( $cust_pkg, emt('Contract ends'), 'contract_end', %opt ) %>
 
 %     if ( $part_pkg->freq and !$supplemental && ! $opt{no_links} ) { 
         <TR>
           <TD COLSPAN=<%$opt{colspan}%>>
             <FONT SIZE=-1>
-%             if ( $curuser->access_right('Suspend customer package') ) { 
-                (&nbsp;<% pkg_suspend_link($cust_pkg) %>&nbsp;)
-%             } 
-%             if ( $curuser->access_right('Suspend customer package later') ) { 
-                (&nbsp;<% pkg_adjourn_link($cust_pkg) %>&nbsp;)
-%             } 
-%             if ( $curuser->access_right('Delay suspension events') ) { 
-                (&nbsp;<% pkg_delay_link($cust_pkg) %>&nbsp;)
-%             } 
+% # action links
+%           if ( $change_from ) {
+%               # nothing
+%           } elsif ( $cust_pkg->change_to_pkgnum ) {
+%               # then you can modify the package change
+%               if ( $curuser->access_right('Change customer package') ) {
+                (&nbsp;<% pkg_change_now_link($cust_pkg) %>&nbsp;)
+                (&nbsp;<% pkg_change_later_link($cust_pkg) %>&nbsp;)
+                (&nbsp;<% pkg_unchange_link($cust_pkg) %>&nbsp;)
+                <BR>
+%               }
+%           }
+
+%           # suspension actions--always available
+%           if ( $curuser->access_right('Suspend customer package') ) { 
+              (&nbsp;<% pkg_suspend_link($cust_pkg) %>&nbsp;)
+%           } 
+%           if ( $curuser->access_right('Suspend customer package later') ) { 
+              (&nbsp;<% pkg_adjourn_link($cust_pkg) %>&nbsp;)
+%           } 
+%           if ( $curuser->access_right('Delay suspension events') ) { 
+              (&nbsp;<% pkg_delay_link($cust_pkg) %>&nbsp;)
+%           }
+%
+%           if ( $change_from or $cust_pkg->change_to_pkgnum ) {
+%               # you can't cancel the package while in this state
+%           } else { # the normal case: links to cancel the package
+              <BR>
 %             if ( $curuser->access_right('Cancel customer package immediately') ) { 
                 (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
-%             } 
+%             }
 %             if ( $curuser->access_right('Cancel customer package later') ) { 
                 (&nbsp;<% pkg_expire_link($cust_pkg) %>&nbsp;)
 %             } 
+%           }
 
             <FONT>
           </TD>
@@ -251,6 +290,7 @@ my $part_pkg = $opt{'part_pkg'};
 my $curuser  = $FS::CurrentUser::CurrentUser;
 my $width    = $opt{'cust_pkg-display_times'} ? '38%' : '56%';
 my $supplemental = $opt{'supplemental'};
+my $change_from  = $opt{'change_from'};
 
 $opt{colspan}  = $opt{'cust_pkg-display_times'} ? 8 : 4;
 
@@ -330,14 +370,41 @@ sub pkg_status_row_if {
          $opt{curuser}->access_right('Suspend customer package later')
        );
 
-  $title = '<FONT SIZE=-1>(&nbsp;'. pkg_unexpire_link($cust_pkg). '&nbsp;)&nbsp;</FONT>'. $title
-    if ( $field eq 'expire' &&
-         $opt{curuser}->access_right('Cancel customer package later')
-       );
-
   $cust_pkg->get($field) ? pkg_status_row($cust_pkg, $title, $field, %opt) : '';
 }
 
+sub pkg_status_row_expire {
+  my $cust_pkg = shift;
+  my %opt = @_;
+  return unless $cust_pkg->get('expire');
+
+  my $title;
+
+  if ( $cust_pkg->get('change_to_pkg') ) {
+    if ( $cust_pkg->change_to_pkg->pkgpart != $cust_pkg->pkgpart ) {
+      $title = mt('Will change to <b>[_1]</b> on',
+                 $cust_pkg->change_to_pkg->part_pkg->pkg);
+    } elsif ( $cust_pkg->change_to_pkg->locationnum != $cust_pkg->locationnum )
+    {
+      $title = mt('Will <b>change location</b> on');
+    } else {
+      # FS::cust_pkg->change_later should have prevented this, but 
+      # just so that we can display _something_
+      $title = '<font color="#ff0000">Unknown package change</font>';
+    }
+
+  } else {
+
+    $title = emt('Expires');
+    if ( $opt{curuser}->access_right('Cancel customer package later')) {
+      $title = '<FONT SIZE=-1>(&nbsp;'. pkg_unexpire_link($cust_pkg). '&nbsp;)&nbsp;</FONT>'. $title;
+    }
+
+  }
+
+  pkg_status_row( $cust_pkg, $title, 'expire', %opt );
+}
+
 sub pkg_status_row_changed {
   my( $cust_pkg, %opt ) = @_;
 
@@ -538,6 +605,8 @@ sub pkg_resume_link {
 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'),     @_ ); }
+sub pkg_unchange_link  { pkg_link('misc/do_not_change_pkg',  emt('Abort change'),     @_ ); }
+sub pkg_change_now_link  { pkg_link('misc/change_pkg_now',  emt('Change now'),     @_ ); }
 
 sub pkg_cancel_link {
   include( '/elements/popup_link-cust_pkg.html',
@@ -569,6 +638,18 @@ sub pkg_expire_link {
          )
 }
 
+sub pkg_change_later_link {
+  my $cust_pkg = shift;
+  include( '/elements/popup_link-cust_pkg.html',
+    'action'      => $p . 'misc/change_pkg.cgi?',
+    'label'       => emt('Reschedule'),
+    'actionlabel' => emt('Edit scheduled change for'),
+    'cust_pkg'    => $cust_pkg,
+    'width'       => 763,
+    'height'      => 480,
+  )
+}
+
 sub svc_recharge_link {
   include( '/elements/popup_link-cust_svc.html',
              'action'      => $p. 'misc/recharge_svc.html',