summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Wells <mark@freeside.biz>2013-07-15 18:37:26 -0700
committerMark Wells <mark@freeside.biz>2013-07-15 18:37:26 -0700
commit2c62268f304f1ec6e8baf89043eb1bd1197bb9a6 (patch)
tree0be3cca186ab9b682181555c8602b1be30d2e670
parent53fbfad948c15a03e1939e3b81e2b5fca5796015 (diff)
future package change, #20687
-rw-r--r--FS/FS/Schema.pm1
-rw-r--r--FS/FS/cust_main/Billing.pm20
-rw-r--r--FS/FS/cust_pkg.pm327
-rw-r--r--httemplate/edit/process/change-cust_pkg.html31
-rw-r--r--httemplate/elements/tr-select-cust-part_pkg.html5
-rwxr-xr-xhttemplate/misc/change_pkg.cgi29
-rwxr-xr-xhttemplate/view/cust_main/packages.html21
-rw-r--r--httemplate/view/cust_main/packages/location.html35
-rw-r--r--httemplate/view/cust_main/packages/package.html26
-rwxr-xr-xhttemplate/view/cust_main/packages/section.html4
-rw-r--r--httemplate/view/cust_main/packages/status.html125
11 files changed, 505 insertions, 119 deletions
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 5e2e2efd5..8d234de81 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -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' => [],
diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm
index 220f66a0c..081dd70f7 100644
--- a/FS/FS/cust_main/Billing.pm
+++ b/FS/FS/cust_main/Billing.pm
@@ -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;
}
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index dd67d0313..5cac458ca 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -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
diff --git a/httemplate/edit/process/change-cust_pkg.html b/httemplate/edit/process/change-cust_pkg.html
index c893f13a2..9d06d8e1a 100644
--- a/httemplate/edit/process/change-cust_pkg.html
+++ b/httemplate/edit/process/change-cust_pkg.html
@@ -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>
diff --git a/httemplate/elements/tr-select-cust-part_pkg.html b/httemplate/elements/tr-select-cust-part_pkg.html
index c9c50d27e..488f04a13 100644
--- a/httemplate/elements/tr-select-cust-part_pkg.html
+++ b/httemplate/elements/tr-select-cust-part_pkg.html
@@ -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'} %>;
+% }
}
diff --git a/httemplate/misc/change_pkg.cgi b/httemplate/misc/change_pkg.cgi
index 03e336cba..7425fbfaf 100755
--- a/httemplate/misc/change_pkg.cgi
+++ b/httemplate/misc/change_pkg.cgi
@@ -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>
@@ -30,6 +29,21 @@
</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>
diff --git a/httemplate/view/cust_main/packages.html b/httemplate/view/cust_main/packages.html
index e32fe4c03..566ab2943 100755
--- a/httemplate/view/cust_main/packages.html
+++ b/httemplate/view/cust_main/packages.html
@@ -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 );
diff --git a/httemplate/view/cust_main/packages/location.html b/httemplate/view/cust_main/packages/location.html
index ab961b79e..01cbc0ffb 100644
--- a/httemplate/view/cust_main/packages/location.html
+++ b/httemplate/view/cust_main/packages/location.html
@@ -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; ',
@@ -22,25 +27,25 @@
</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;
diff --git a/httemplate/view/cust_main/packages/package.html b/httemplate/view/cust_main/packages/package.html
index 7aad9a44e..596a47391 100644
--- a/httemplate/view/cust_main/packages/package.html
+++ b/httemplate/view/cust_main/packages/package.html
@@ -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>
@@ -30,7 +29,11 @@
% 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",
@@ -185,6 +188,7 @@
% )
% {
<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,
);
}
diff --git a/httemplate/view/cust_main/packages/section.html b/httemplate/view/cust_main/packages/section.html
index 82d06203b..0383fe892 100755
--- a/httemplate/view/cust_main/packages/section.html
+++ b/httemplate/view/cust_main/packages/section.html
@@ -36,6 +36,10 @@
<& 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) {
diff --git a/httemplate/view/cust_main/packages/status.html b/httemplate/view/cust_main/packages/status.html
index ed360cca4..6894a4e02 100644
--- a/httemplate/view/cust_main/packages/status.html
+++ b/httemplate/view/cust_main/packages/status.html
@@ -76,18 +76,29 @@
<% 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>
@@ -97,9 +108,17 @@
%
% } 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 ) %>
@@ -193,7 +212,7 @@
% }
%
-% }
+% }
%
% if ( $opt{'cust_pkg-show_autosuspend'} ) {
% my $autosuspend = pkg_autosuspend_time( $cust_pkg );
@@ -207,7 +226,7 @@
<% 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} ) {
@@ -215,21 +234,41 @@
<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',