automatic package changes for supplemental packages, #37102
authorMark Wells <mark@freeside.biz>
Tue, 14 Jul 2015 00:26:48 +0000 (17:26 -0700)
committerMark Wells <mark@freeside.biz>
Tue, 14 Jul 2015 20:25:05 +0000 (13:25 -0700)
14 files changed:
FS/FS/Schema.pm
FS/FS/cust_pkg.pm
FS/FS/part_pkg.pm
FS/FS/part_pkg/flat.pm
httemplate/browse/part_pkg.cgi
httemplate/edit/part_pkg.cgi
httemplate/elements/freeside.css
httemplate/elements/select.html
httemplate/elements/tr-select-expire_months.html [new file with mode: 0644]
httemplate/elements/tr-select-months.html [new file with mode: 0644]
httemplate/view/cust_main/packages.html
httemplate/view/cust_main/packages/package.html
httemplate/view/cust_main/packages/section.html
httemplate/view/cust_main/packages/status.html

index eb5f1d3..c8b9b63 100644 (file)
@@ -3198,6 +3198,10 @@ sub tables_hashref {
         'delay_start',   'int',     'NULL', '', '', '',
         'start_on_hold', 'char',    'NULL',  1, '', '',
         'agent_pkgpartid', 'varchar', 'NULL', 20, '', '',
+        'expire_months', 'int',     'NULL', '', '', '',
+        'adjourn_months', 'int',    'NULL', '', '', '',
+        'contract_end_months','int','NULL', '', '', '',
+        'change_to_pkgpart', 'int', 'NULL', '', '', '',
       ],
       'primary_key'  => 'pkgpart',
       'unique'       => [],
@@ -3226,6 +3230,10 @@ sub tables_hashref {
                             table      => 'part_pkg',
                             references => [ 'pkgpart' ],
                           },
+                          { columns    => [ 'change_to_pkgpart' ],
+                            table      => 'part_pkg',
+                            references => [ 'pkgpart' ],
+                          },
                         ],
     },
 
index 950d348..fbecd8d 100644 (file)
@@ -251,19 +251,53 @@ or contract_end timers to some number of months after the start date
 a delayed setup fee after a period of "free days", will also set the 
 start date to the end of that period.
 
+If the package has an automatic transfer rule (C<change_to_pkgnum>), then
+this will also order the package and set its start date.
+
 =cut
 
 sub set_initial_timers {
   my $self = shift;
   my $part_pkg = $self->part_pkg;
+  my $start = $self->start_date || $self->setup || time;
+
   foreach my $action ( qw(expire adjourn contract_end) ) {
-    my $months = $part_pkg->option("${action}_months",1);
+    my $months = $part_pkg->get("${action}_months");
     if($months and !$self->get($action)) {
-      my $start = $self->start_date || $self->setup || time;
       $self->set($action, $part_pkg->add_freq($start, $months) );
     }
   }
 
+  # if this package has an expire date and a change_to_pkgpart, set automatic
+  # package transfer
+  # (but don't call change_later, as that would call $self->replace, and we're
+  # probably in the middle of $self->insert right now)
+  if ( $part_pkg->expire_months and $part_pkg->change_to_pkgpart ) {
+    if ( $self->change_to_pkgnum ) {
+      # this can happen if a package is ordered on hold, scheduled for a 
+      # future change _while on hold_, and then released from hold, causing
+      # the automatic transfer to schedule.
+      #
+      # what's correct behavior in that case? I think it's to disallow
+      # future-changing an on-hold package that has an automatic transfer.
+      # but if we DO get into this situation, let the manual package change
+      # win.
+      warn "pkgnum ".$self->pkgnum.": manual future package change blocks ".
+           "automatic transfer.\n";
+    } else {
+      my $change_to = FS::cust_pkg->new( {
+          start_date  => $self->get('expire'),
+          pkgpart     => $part_pkg->change_to_pkgpart,
+          map { $_ => $self->get($_) }
+            qw( custnum locationnum quantity refnum salesnum contract_end )
+      } );
+      my $error = $change_to->insert;
+
+      return $error if $error;
+      $self->set('change_to_pkgnum', $change_to->pkgnum);
+    }
+  }
+
   # if this package has "free days" and delayed setup fee, then
   # set start date that many days in the future.
   # (this should have been set in the UI, but enforce it here)
@@ -273,6 +307,7 @@ sub set_initial_timers {
   {
     $self->start_date( $part_pkg->default_start_date );
   }
+
   '';
 }
 
@@ -332,9 +367,12 @@ a location change).
 sub insert {
   my( $self, %options ) = @_;
 
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
   my $error;
   $error = $self->check_pkgpart unless $options{'allow_pkgpart'};
-  return $error if $error;
 
   my $part_pkg = $self->part_pkg;
 
@@ -359,15 +397,12 @@ sub insert {
       $self->set('start_date', '');
     } else {
       # set expire/adjourn/contract_end timers, and free days, if appropriate
-      $self->set_initial_timers;
+      # and automatic package transfer, which can fail, so capture the result
+      $error = $self->set_initial_timers;
     }
   } # else this is a package change, and shouldn't have "new package" behavior
 
-  my $oldAutoCommit = $FS::UID::AutoCommit;
-  local $FS::UID::AutoCommit = 0;
-  my $dbh = dbh;
-
-  $error = $self->SUPER::insert($options{options} ? %{$options{options}} : ());
+  $error ||= $self->SUPER::insert($options{options} ? %{$options{options}} : ());
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -461,9 +496,26 @@ hide cancelled packages.
 
 =cut
 
+# this is still used internally to abort future package changes, so it 
+# does need to work
+
 sub delete {
   my $self = shift;
 
+  # The following foreign keys to cust_pkg are not cleaned up here, and will
+  # cause package deletion to fail:
+  #
+  # cust_credit.pkgnum and commission_pkgnum (and cust_credit_void)
+  # cust_credit_bill.pkgnum
+  # cust_pay_pending.pkgnum
+  # cust_pay.pkgnum (and cust_pay_void)
+  # cust_bill_pay.pkgnum (wtf, shouldn't reference pkgnum)
+  # cust_pkg_usage.pkgnum
+  # cust_pkg.uncancel_pkgnum, change_pkgnum, main_pkgnum, and change_to_pkgnum
+
+  # cust_svc is handled by canceling the package before deleting it
+  # cust_pkg_option is handled via option_Common
+
   my $oldAutoCommit = $FS::UID::AutoCommit;
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
@@ -499,7 +551,13 @@ sub delete {
     }
   }
 
-  #pkg_referral?
+  foreach my $pkg_referral ( $self->pkg_referral ) {
+    my $error = $pkg_referral->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
 
   my $error = $self->SUPER::delete(@_);
   if ( $error ) {
@@ -807,12 +865,15 @@ sub cancel {
   my( $self, %options ) = @_;
   my $error;
 
-  # pass all suspend/cancel actions to the main package
-  # (unless the pkglinknum has been removed, then the link is defunct and
-  # this package can be canceled on its own)
-  if ( $self->main_pkgnum and $self->pkglinknum and !$options{'from_main'} ) {
-    return $self->main_pkg->cancel(%options);
-  }
+  # supplemental packages can now be separately canceled, though the UI
+  # shouldn't permit it
+  #
+  ## pass all suspend/cancel actions to the main package
+  ## (unless the pkglinknum has been removed, then the link is defunct and
+  ## this package can be canceled on its own)
+  #if ( $self->main_pkgnum and $self->pkglinknum and !$options{'from_main'} ) {
+  #  return $self->main_pkg->cancel(%options);
+  #}
 
   my $conf = new FS::Conf;
 
@@ -936,8 +997,14 @@ sub cancel {
     $hash{main_pkgnum} = '';
   }
 
+  # if there is a future package change scheduled, unlink from it (like
+  # abort_change) first, then delete it.
+  $hash{'change_to_pkgnum'} = '';
+
+  # save the package state
   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('no_delay_cancel' => 1) || $change_to->delete;
@@ -1285,9 +1352,13 @@ sub suspend {
   my( $self, %options ) = @_;
   my $error;
 
-  # pass all suspend/cancel actions to the main package
+  # supplemental packages still can't be separately suspended, but silently
+  # exit instead of failing or passing the action to the main package (so
+  # that the "Suspend customer" action doesn't trip over the supplemental
+  # packages and die)
+
   if ( $self->main_pkgnum and !$options{'from_main'} ) {
-    return $self->main_pkg->suspend(%options);
+    return;
   }
 
   my $oldAutoCommit = $FS::UID::AutoCommit;
@@ -1659,7 +1730,11 @@ sub unsuspend {
 
   if (!$self->setup) {
     # then this package is being released from on-hold status
-    $self->set_initial_timers;
+    $error = $self->set_initial_timers;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
   }
 
   my @labels = ();
@@ -2034,12 +2109,12 @@ sub change {
     # almost. if the new pkgpart specifies start/adjourn/expire timers, 
     # apply those.
     if ( $opt->{'pkgpart'} and $opt->{'pkgpart'} != $self->pkgpart ) {
-      $self->set_initial_timers;
+      $error ||= $self->set_initial_timers;
     }
     # but if contract_end was explicitly specified, that overrides all else
     $self->set('contract_end', $opt->{'contract_end'})
       if $opt->{'contract_end'};
-    $error = $self->replace;
+    $error ||= $self->replace;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return "modifying package: $error";
@@ -2509,16 +2584,28 @@ Cancels a future package change scheduled by C<change_later>.
 
 sub abort_change {
   my $self = shift;
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+
   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;
+  $error = $self->replace;
+  if ( $change_to ) {
+    $error ||= $change_to->cancel || $change_to->delete;
+  }
+
+  if ( $oldAutoCommit ) {
+    if ( $error ) {
+      dbh->rollback;
+    } else {
+      dbh->commit;
+    }
+  }
+
+  return $error;
 }
 
 =item set_quantity QUANTITY
index 0e9ee05..498da8a 100644 (file)
@@ -127,6 +127,18 @@ part_pkg, will be equal to pkgpart.
 ordered. The package will not start billing or have a setup fee charged 
 until it is manually unsuspended.
 
+=item change_to_pkgpart - When this package is ordered, schedule a future 
+package change. The 'expire_months' field will determine when the package
+change occurs.
+
+=item expire_months - Number of months until this package expires (or changes
+to another package).
+
+=item adjourn_months - Number of months until this package becomes suspended.
+
+=item contract_end_months - Number of months until the package's contract 
+ends.
+
 =back
 
 =head1 METHODS
@@ -722,6 +734,11 @@ sub check {
     || $self->ut_numbern('delay_start')
     || $self->ut_foreign_keyn('successor', 'part_pkg', 'pkgpart')
     || $self->ut_foreign_keyn('family_pkgpart', 'part_pkg', 'pkgpart')
+    || $self->ut_numbern('expire_months')
+    || $self->ut_numbern('adjourn_months')
+    || $self->ut_numbern('contract_end_months')
+    || $self->ut_numbern('change_to_pkgpart')
+    || $self->ut_foreign_keyn('change_to_pkgpart', 'part_pkg', 'pkgpart')
     || $self->ut_alphan('agent_pkgpartid')
     || $self->SUPER::check
   ;
@@ -1696,6 +1713,19 @@ for this package.
 Returns the voice usage pools (see L<FS::part_pkg_usage>) defined for 
 this package.
 
+=item change_to_pkg
+
+Returns the automatic transfer target for this package, or an empty string
+if there isn't one.
+
+=cut
+
+sub change_to_pkg {
+  my $self = shift;
+  my $pkgpart = $self->change_to_pkgpart or return '';
+  FS::part_pkg->by_key($pkgpart);
+}
+
 =item _rebless
 
 Reblesses the object into the FS::part_pkg::PLAN class (if available), where
@@ -2202,6 +2232,19 @@ sub queueable_upgrade {
     FS::upgrade_journal->set_done($upgrade);
   }
 
+  # migrate adjourn_months, expire_months, and contract_end_months to 
+  # real fields
+  foreach my $field (qw(adjourn_months expire_months contract_end_months)) {
+    foreach my $option (qsearch('part_pkg_option', { optionname => $field })) {
+      my $part_pkg = $option->part_pkg;
+      my $error = $option->delete;
+      if ( $option->optionvalue and $part_pkg->get($field) eq '' ) {
+        $part_pkg->set($field, $option->optionvalue);
+        $error ||= $part_pkg->replace;
+      }
+      die $error if $error;
+    }
+  }
 }
 
 =item curuser_pkgs_sql
index eb70253..d11b99b 100644 (file)
@@ -34,16 +34,6 @@ tie my %contract_years, 'Tie::IxHash', (
                              'select_options' => \%temporalities,
                            },
 
-    #used in cust_pkg.pm so could add to any price plan
-    'expire_months' => { 'name' => 'Auto-add an expiration date this number of months out',
-                       },
-    'adjourn_months'=> { 'name' => 'Auto-add a suspension date this number of months out',
-                       },
-    'contract_end_months'=> { 
-                        'name' => 'Auto-add a contract end date this number of years out',
-                        'type' => 'select',
-                        'select_options' => \%contract_years,
-                      },
     #used in cust_pkg.pm so could add to any price plan where it made sense
     'start_1st'     => { 'name' => 'Auto-add a start date to the 1st, ignoring the current month.',
                          'type' => 'checkbox',
@@ -85,8 +75,6 @@ tie my %contract_years, 'Tie::IxHash', (
                     },
   },
   'fieldorder' => [ qw( recur_temporality 
-                        expire_months adjourn_months
-                        contract_end_months
                         start_1st
                         sync_bill_date prorate_defer_bill prorate_round_day
                         suspend_bill unsuspend_adjust_bill
index f8de620..c2f1430 100755 (executable)
@@ -247,6 +247,7 @@ push @fields, sub {
                   $part_pkg->part_pkg_discount;
 
   [
+    # Line 0: Family package link (if applicable)
     ( !$family_pkgpart &&
       $part_pkg->pkgpart == $part_pkg->family_pkgpart ? () : [
       {
@@ -257,13 +258,13 @@ push @fields, sub {
         'link' => $p.'browse/part_pkg.cgi?family='.$part_pkg->family_pkgpart,
       }
     ] ),
-    [
+    [ # Line 1: Plan type (Anniversary, Prorate, Call Rating, etc.)
       { data =>$plan,
         align=>'center',
         colspan=>2,
       },
     ],
-    [
+    [ # Line 2: Setup fee
       { data =>$money_char.
                sprintf('%.2f ', $part_pkg->option('setup_fee') ),
         align=>'right'
@@ -278,7 +279,7 @@ push @fields, sub {
         align=>'left',
       },
     ],
-    [
+    [ # Line 3: Recurring fee
       { data=>(
           $is_recur
             ? $money_char. sprintf('%.2f', $part_pkg->option('recur_fee'))
@@ -288,20 +289,56 @@ push @fields, sub {
         colspan=> ( $is_recur ? 1 : 2 ),
       },
       ( $is_recur
-        ?  { data => ( $is_recur
-               ? ' &nbsp; '. $part_pkg->freq_pretty.
-                 ( $part_pkg->option('recur_fee') == 0
-                     && $part_pkg->recur_show_zero
-                   ? ' (printed on invoices)'
-                   : ''
-                 )
-               : '' ),
+        ?  { data => ' &nbsp; '. $part_pkg->freq_pretty.
+                     ( $part_pkg->option('recur_fee') == 0
+                         && $part_pkg->recur_show_zero
+                       ? ' (printed on invoices)'
+                       : ''
+                     ),
              align=>'left',
            }
         : ()
       ),
     ],
-    (
+    [ { data => '&nbsp;' }, ], # Line 4: empty
+    ( $part_pkg->adjourn_months ? 
+      [ # Line 5: Adjourn months
+        { data => mt('After [quant,_1,month], <strong>suspend</strong> the package.',
+                     $part_pkg->adjourn_months),
+          align => 'left',
+          size  => -1,
+          colspan => 2,
+        }
+      ] : ()
+    ),
+    ( $part_pkg->contract_end_months ? 
+      [ # Line 6: Contract end months
+        { data => mt('After [quant,_1,month], <strong>contract ends</strong>.',
+                     $part_pkg->contract_end_months),
+          align => 'left',
+          size  => -1,
+          colspan => 2,
+        }
+      ] : ()
+    ),
+    ( $part_pkg->expire_months ? 
+      [ # Line 7: Expire months and automatic transfer
+        { data => $part_pkg->change_to_pkgpart ?
+                    mt('After [quant,_1,month], <strong>change to</strong> ',
+                      $part_pkg->expire_months) .
+                    qq(<a href="${p}edit/part_pkg.cgi?) .
+                      $part_pkg->change_to_pkgpart .
+                      qq(">) . $part_pkg->change_to_pkg->pkg . qq(</a>) . '.'
+                  : mt('After [quant,_1,month], <strong>cancel</strong> the package.',
+                     $part_pkg->expire_months)
+          ,
+          align => 'left',
+          size  => -1,
+          colspan => 2,
+        }
+      ] : ()
+    ),
+    ( # Usage prices
       map { my $amount = $_->amount / ($_->target_info->{multiplier} || 1);
             my $label = $_->target_info->{label};
             [
@@ -315,7 +352,8 @@ push @fields, sub {
           }
         $part_pkg->part_pkg_usageprice
     ),
-    ( map { my $dst_pkg = $_->dst_pkg;
+    ( # Supplementals
+      map { my $dst_pkg = $_->dst_pkg;
             [
               { data => 'Supplemental: &nbsp;'.
                         '<A HREF="#'. $dst_pkg->pkgpart . '">' .
@@ -327,7 +365,8 @@ push @fields, sub {
           }
       $part_pkg->supp_part_pkg_link
     ),
-    ( map { 
+    ( # Billing add-ons/bundle packages
+      map { 
             my $dst_pkg = $_->dst_pkg;
             [ 
               { data => 'Add-on:&nbsp;'.$dst_pkg->pkg_comment,
@@ -338,7 +377,8 @@ push @fields, sub {
           }
       $part_pkg->bill_part_pkg_link
     ),
-    ( scalar(@discounts)
+    ( # Discounts available
+      scalar(@discounts)
         ?  [ 
               { data => '<b>Discounts</b>',
                 align=>'center', #?
@@ -360,7 +400,7 @@ push @fields, sub {
           @discounts
         : ()
     ),
-  ];
+  ]; # end of "middle column"
 
 #  $plan_labels{$part_pkg->plan}.'<BR>'.
 #    $money_char.sprintf('%.2f setup<BR>', $part_pkg->option('setup_fee') ).
index a90a625..9f5510d 100755 (executable)
@@ -28,7 +28,7 @@
 
      'onsubmit'              => 'confirm_submit',
 
-     'labels' => { 
+     'labels' => {
                    'pkgpart'          => 'Package Definition',
                    'pkg'              => 'Package',
                    %locale_field_labels,
                    'supp_dst_pkgpart' => 'When ordering package, also order',
                    'report_option'    => 'Report classes',
                    'delay_start'      => 'Default delay (days)',
+                   'adjourn_months'   => 'Suspend the package after ',
+                   'contract_end_months' => 'Contract ends after ',
+                   'expire_months'    => 'Cancel the package after ',
+                   'change_to_pkgpart'=> 'and replace it with ',
                  },
 
      'fields' => [
                          sort $conf->config('currencies')
                      ),
 
+                     ( $conf->exists('part_pkg-delay_start')
+                       ? ( { type  => 'tablebreak-tr-title',
+                             value => 'Delayed start',
+                           },
+                           { field => 'delay_start',
+                             type => 'text', size => 6 },
+                         )
+                       : ()
+                     ),
+
+                     { type   => 'tablebreak-tr-title',
+                       value  => 'Limited duration',
+                     },
+                     { field  => 'adjourn_months',
+                       type   => 'select-months',
+                     },
+                     { field  => 'contract_end_months',
+                       type   => 'select-months',
+                     },
+                     { field  => 'expire_months',
+                       type   => 'select-expire_months',
+                     },
+                     { field  => 'change_to_pkgpart',
+                       type   => 'select-part_pkg',
+                       extra_sql  => sub { $pkgpart
+                        ? "AND pkgpart != $pkgpart"
+                        : ''
+                       },
+                       empty_label => 'no package',
+                     },
+
                      #price plan
                      #setup fee
                      #recurring frequency
                          )
                      ),
 
-                     ( $conf->exists('part_pkg-delay_start')
-                       ? ( { type  => 'tablebreak-tr-title',
-                             value => 'Delayed start',
-                           },
-                           { field => 'delay_start',
-                             type => 'text', size => 6 },
-                         )
-                       : ()
-                     ),
-
                    { type => 'columnnext' },
 
                      {type=>'justtitle', value=>'Agent (reseller) types' },
index d4e155a..dbd27cb 100644 (file)
@@ -323,3 +323,15 @@ div#overDiv {
   box-shadow: #333333 1px 1px 2px;
 }
 
+/* view/cust_main/packages/package.html */
+div.package-marker-supplemental {
+  height: 100%;
+  border-left: solid #bbbbff 30px;
+  display: inline-block;
+}
+
+div.package-marker-change_from {
+  height: 100%;
+  border-left: solid #bbffbb 30px;
+  display: inline-block;
+}
index 4492681..42cd895 100644 (file)
@@ -1,3 +1,29 @@
+<%doc>
+<& select.html,
+  # required
+    field       => 'myfield', # NAME property
+    curr_value  => 'foo',
+    labels      => { # or 'option_labels'
+                     'AL' => 'Alabama',
+                     'AK' => 'Alaska',
+                     'AR' => 'Arkansas',
+                   },
+    options     => [ 'AL', 'AK', 'AR' ],
+    curr_value  => $cgi->param('myfield'),
+
+  # recommended    
+    id          => 'myid',    # DOM id
+
+  # optional
+    size        => 1,         # to show multiple rows at once
+    style       => '',        # STYLE property
+    multiple    => 0,
+    disabled    => 0,
+    onchange    => 'do_something()',
+    js_only     => 0,         # disables the whole thing
+&>
+</%doc>
+    
 % unless ( $opt{'js_only'} ) {
 
 <SELECT NAME          = "<% $opt{field} %>"
diff --git a/httemplate/elements/tr-select-expire_months.html b/httemplate/elements/tr-select-expire_months.html
new file mode 100644 (file)
index 0000000..ced9660
--- /dev/null
@@ -0,0 +1,10 @@
+<& tr-select-months.html, @_ &>
+<script>
+// disable the pkgpart selector if it's set to zero months
+$().ready(function() {
+  $('#expire_months').on('change', function() {
+    $('#change_to_pkgpart').prop('disabled', this.value == 0);
+  })
+  .trigger('change');
+});
+</script>
diff --git a/httemplate/elements/tr-select-months.html b/httemplate/elements/tr-select-months.html
new file mode 100644 (file)
index 0000000..3ff28f9
--- /dev/null
@@ -0,0 +1,12 @@
+<%init>
+my %opt = @_;
+$opt{id} ||= $opt{field}; # should be the default everywhere
+my $max = $opt{max} || 36;
+$opt{options} = [ '', 1 .. $max ];
+$opt{labels} = { '' => '',
+                 map { $_ => emt('[quant,_1,month]', $_) } 1 .. $max
+               };
+
+warn Dumper(\%opt);
+</%init>
+<& tr-select.html, %opt &>
index 4131570..4903e18 100755 (executable)
@@ -180,8 +180,11 @@ my @packages = $cust_main->all_pkgs( {
   },
 } );
 
+my $is_anything_hidden = 0; # optimization
+
 my %change_to_from; # target pkgnum => current cust_pkg, for future changes
 my %changed_from; # old pkgnum => new cust_pkg, for past changes
+my %supplementals_of; # main pkgnum => arrayref of supplementals
 
 foreach my $cust_pkg ( @packages ) {
   my %hash = $cust_pkg->hash;
@@ -190,18 +193,33 @@ foreach my $cust_pkg ( @packages ) {
   $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;
+    $is_anything_hidden = 1;
   }
   if ( $cust_pkg->change_pkgnum ) {
     $changed_from{$cust_pkg->change_pkgnum} = $cust_pkg;
+    $is_anything_hidden = 1;
+  }
+  if ( $cust_pkg->main_pkgnum ) {
+    $supplementals_of{$cust_pkg->main_pkgnum} ||= [];
+    push @{ $supplementals_of{$cust_pkg->main_pkgnum} }, $cust_pkg;
+    $is_anything_hidden = 1;
   }
 }
 
 # filter out hidden package changes
-if ( keys %change_to_from or keys %changed_from ) {
+if ( $is_anything_hidden ) {
   my @displayable_packages;
   foreach my $cust_pkg (@packages) {
 
-    if ( exists( $change_to_from{$cust_pkg->pkgnum} ) ) {
+    # if this package has any supplemental packages, it should remember them
+    $cust_pkg->set('_supplemental', $supplementals_of{$cust_pkg->pkgnum});
+
+    if ( $cust_pkg->main_pkgnum ) {
+
+      # it's a supplemental package of something else, and shouldn't be on the
+      # root list
+
+    } elsif ( exists( $change_to_from{$cust_pkg->pkgnum} ) ) {
 
       # $cust_pkg is an ordered, not-yet-active package change target
       my $change_from = $change_to_from{ $cust_pkg->pkgnum };
@@ -217,7 +235,9 @@ if ( keys %change_to_from or keys %changed_from ) {
       $changed_to->set('changed_from_pkg', $cust_pkg);
 
     } else {
+
       push @displayable_packages, $cust_pkg;
+
     }
 
   }
@@ -252,7 +272,7 @@ $num_old_packages -= scalar(@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 = grep !$_->main_pkgnum, @packages;
 
 foreach my $cust_pkg ( @packages ) {
   $cust_pkg->{'_cust_pkg_discount_active'} =
index 4b56e6f..8aa6403 100644 (file)
@@ -1,7 +1,6 @@
-<TD CLASS="inv package" BGCOLOR="<% $bgcolor %>" VALIGN="top" <%$style%>>
+<TD CLASS="inv package" BGCOLOR="<% $bgcolor %>" VALIGN="top">
+  <% join('', @marker ) %>
   <TABLE CLASS="inv package"> 
-
-
     <TR>
       <TD COLSPAN=2>
         <% $opt{before_pkg_callback}
 %              ) {
               (&nbsp;<%pkg_event_link($cust_pkg)%>&nbsp;)
 %           }
-%         } #!$supplemental
+%         } # a canceled recurring package, or else no_links is in effect
 
         </FONT>
       </TD>
   </TABLE>
 % }
 
+  <% join('', map '</DIV>', @marker ) %>
 </TD>
 
 <%init>
@@ -317,16 +317,12 @@ my $statedefault   = $opt{'statedefault'}
 # 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"!;
+my @marker;
+if ( $supplemental ) {
+  push @marker, '<DIV CLASS="package-marker-supplemental">';
+}
+if ( $change_from ) {
+  push @marker, '<DIV CLASS="package-marker-change_from">';
 }
 
 $cust_pkg->pkgnum =~ /^(\d+)$/;
index fe9f283..490f09c 100755 (executable)
     <& .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) {
-    <& .packagerow, $supp_pkg, %iopt &>
+% if ( $cust_pkg->_supplemental ) {
+%   $iopt{'supplemental'} = ($iopt{'supplemental'} || 0) + 1;
+%   foreach my $supp_pkg (@{ $cust_pkg->_supplemental }) {
+      <& .packagerow, $supp_pkg, %iopt &>
+%   }
 % }
 </%def>
 <%shared>
index 81156c9..7e125f7 100644 (file)
       </TR>
 %   }
 %
-% } else {
+% } else { # not canceled
 %
 %   if ( $cust_pkg->get('susp') ) { #suspended or on hold
 %
-%     #if ( $cust_pkg->order_date eq $cust_pkg->get('susp') ) # inconsistent with FS::cust_pkg::status
+%     #if ( $cust_pkg->order_date eq $cust_pkg->get('susp') ) # inconsistent with FS::cust_pkg::status
 %     if ( ! $cust_pkg->setup ) { #status: on hold
 
         <% pkg_status_row( $cust_pkg, emt('On Hold'), '', 'color'=>'7E0079', %opt ) %>
@@ -79,7 +79,7 @@
 %     } else { 
         <% pkg_status_row($cust_pkg, emt('Setup'), 'setup', %opt ) %>
 %     }
-%   } 
+%   }
 
     <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
 
     <% 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} && !$change_from ) {
+%   # Status changes for suspended packages: can unsuspend, future-unsuspend,
+%   # or future-change. If this package is a future change or is supplemental
+%   # disable them all.
+%   if ( !$supplemental && ! $opt{no_links} && !$change_from ) {
       <TR>
         <TD COLSPAN=<%$opt{colspan}%>>
           <FONT SIZE=-1>
 
           <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
 
-%       } else { 
+%       } else { # recurring package
 %
 %         my $num_cust_svc = $cust_pkg->num_cust_svc;
 %         my $summarize = $opt{'cust_pkg-large_pkg_size'} > 0
       <% 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} ) { 
+%     # Status changes for active recurring packages. If it has a future
+%     # package change scheduled, let that be modified. If it's supplemental,
+%     # then that's the only allowed action. Otherwise allow suspend, future
+%     # suspend, do-not-suspend, and immediate and future cancel.
+%     if ( $part_pkg->freq and ! $opt{no_links} ) { 
 
         <TR>
           <TD COLSPAN=<%$opt{colspan}%>>
 %               }
 %           }
 
+%           if ( !$supplemental ) {
 %           # 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 ( $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 ( $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;)
+%               }
 %             }
-%             if ( $curuser->access_right('Cancel customer package later') ) { 
-                (&nbsp;<% pkg_expire_link($cust_pkg) %>&nbsp;)
-%             } 
 %           }
 
             <FONT>