[freeside-commits] branch master updated. eb265def46e7010331161ad303de715c206eca16

Mark Wells mark at 420.am
Tue Jul 14 13:25:59 PDT 2015


The branch, master has been updated
       via  eb265def46e7010331161ad303de715c206eca16 (commit)
       via  98f6d91ec7eaa907204afbfeb90ede1e3bff656d (commit)
      from  57e3a0e08b81d52851314c60f37115a05b9be79e (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit eb265def46e7010331161ad303de715c206eca16
Author: Mark Wells <mark at freeside.biz>
Date:   Tue Jul 14 13:18:49 2015 -0700

    allow non-integer ratios of supplemental package period, #37102

diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm
index df7e17f..0bc0fbd 100644
--- a/FS/FS/cust_main/Billing.pm
+++ b/FS/FS/cust_main/Billing.pm
@@ -1133,19 +1133,39 @@ sub _make_lines {
         # its frequency
         my $main_pkg_freq = $main_pkg->part_pkg->freq;
         my $supp_pkg_freq = $part_pkg->freq;
-        my $ratio = $supp_pkg_freq / $main_pkg_freq;
-        if ( $ratio != int($ratio) ) {
+        if ( $supp_pkg_freq == 0 or $main_pkg_freq == 0 ) {
           # the UI should prevent setting up packages like this, but just
           # in case
-          return "supplemental package period is not an integer multiple of main  package period";
+          return "unable to calculate supplemental package period ratio";
         }
-        $next_bill = $sdate;
-        for (1..$ratio) {
-          $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq );
+        my $ratio = $supp_pkg_freq / $main_pkg_freq;
+        if ( $ratio == int($ratio) ) {
+          # simple case: main package is X months, supp package is X*A months,
+          # advance supp package to where the main package will be in A cycles.
+          $next_bill = $sdate;
+          for (1..$ratio) {
+            $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq );
+          }
+        } else {
+          # harder case: main package is X months, supp package is Y months.
+          # advance supp package by Y months. then if they're within half a 
+          # month of each other, resync them. this may result in the period
+          # not being exactly Y months.
+          $next_bill = $part_pkg->add_freq( $sdate, $supp_pkg_freq );
+          my $main_next_bill = $main_pkg->bill;
+          if ( $main_pkg->bill <= $time ) {
+            # then the main package has not yet been billed on this cycle;
+            # predict what its bill date will be.
+            $main_next_bill =
+              $part_pkg->add_freq( $main_next_bill, $main_pkg_freq );
+          }
+          if ( abs($main_next_bill - $next_bill) < 86400*15 ) {
+            $next_bill = $main_next_bill;
+          }
         }
 
       } else {
-        # the normal case
+      # the normal case, not a supplemental package
       $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
       return "unparsable frequency: ". $part_pkg->freq
         if $next_bill == -1;
diff --git a/FS/FS/part_pkg_link.pm b/FS/FS/part_pkg_link.pm
index ce071ef..5fe6f2f 100644
--- a/FS/FS/part_pkg_link.pm
+++ b/FS/FS/part_pkg_link.pm
@@ -250,12 +250,10 @@ sub check {
     my $dst_pkg = $self->dst_pkg;
     if ( $src_pkg->freq eq '0' and $dst_pkg->freq ne '0' ) {
       return "One-time charges can't have supplemental packages."
-    } elsif ( $dst_pkg->freq ne '0' ) {
-      my $ratio = $dst_pkg->freq / $src_pkg->freq;
-      if ($ratio != int($ratio)) {
-        return "Supplemental package period (pkgpart ".$dst_pkg->pkgpart.
-               ") must be an integer multiple of main package period.";
-      }
+    } elsif ( $dst_pkg->freq == 0 ) {
+      return "The billing period of a supplemental package must be a whole number of months.";
+    } elsif ( $src_pkg->freq == 0 ) {
+      return "To have supplemental packages, the billing period of a package must be a whole number of months.";
     }
   }
 

commit 98f6d91ec7eaa907204afbfeb90ede1e3bff656d
Author: Mark Wells <mark at freeside.biz>
Date:   Mon Jul 13 17:26:48 2015 -0700

    automatic package changes for supplemental packages, #37102

diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index eb5f1d3..c8b9b63 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -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' ],
+                          },
                         ],
     },
 
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index 950d348..fbecd8d 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -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
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 0e9ee05..498da8a 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -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
diff --git a/FS/FS/part_pkg/flat.pm b/FS/FS/part_pkg/flat.pm
index eb70253..d11b99b 100644
--- a/FS/FS/part_pkg/flat.pm
+++ b/FS/FS/part_pkg/flat.pm
@@ -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
diff --git a/httemplate/browse/part_pkg.cgi b/httemplate/browse/part_pkg.cgi
index f8de620..c2f1430 100755
--- a/httemplate/browse/part_pkg.cgi
+++ b/httemplate/browse/part_pkg.cgi
@@ -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
-               ? '   '. $part_pkg->freq_pretty.
-                 ( $part_pkg->option('recur_fee') == 0
-                     && $part_pkg->recur_show_zero
-                   ? ' (printed on invoices)'
-                   : ''
-                 )
-               : '' ),
+        ?  { data => '   '. $part_pkg->freq_pretty.
+                     ( $part_pkg->option('recur_fee') == 0
+                         && $part_pkg->recur_show_zero
+                       ? ' (printed on invoices)'
+                       : ''
+                     ),
              align=>'left',
            }
         : ()
       ),
     ],
-    (
+    [ { data => ' ' }, ], # 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:  '.
                         '<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: '.$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') ).
diff --git a/httemplate/edit/part_pkg.cgi b/httemplate/edit/part_pkg.cgi
index a90a625..9f5510d 100755
--- a/httemplate/edit/part_pkg.cgi
+++ b/httemplate/edit/part_pkg.cgi
@@ -28,7 +28,7 @@
 
      'onsubmit'              => 'confirm_submit',
 
-     'labels' => { 
+     'labels' => {
                    'pkgpart'          => 'Package Definition',
                    'pkg'              => 'Package',
                    %locale_field_labels,
@@ -69,6 +69,10 @@
                    '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' => [
@@ -164,6 +168,37 @@
                          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
@@ -219,16 +254,6 @@
                          )
                      ),
 
-                     ( $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' },
diff --git a/httemplate/elements/freeside.css b/httemplate/elements/freeside.css
index d4e155a..dbd27cb 100644
--- a/httemplate/elements/freeside.css
+++ b/httemplate/elements/freeside.css
@@ -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;
+}
diff --git a/httemplate/elements/select.html b/httemplate/elements/select.html
index 4492681..42cd895 100644
--- a/httemplate/elements/select.html
+++ b/httemplate/elements/select.html
@@ -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
index 0000000..ced9660
--- /dev/null
+++ b/httemplate/elements/tr-select-expire_months.html
@@ -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
index 0000000..3ff28f9
--- /dev/null
+++ b/httemplate/elements/tr-select-months.html
@@ -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 &>
diff --git a/httemplate/view/cust_main/packages.html b/httemplate/view/cust_main/packages.html
index 4131570..4903e18 100755
--- a/httemplate/view/cust_main/packages.html
+++ b/httemplate/view/cust_main/packages.html
@@ -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)
- at packages = grep !$_->main_pkgnum, @packages;
+####@packages = grep !$_->main_pkgnum, @packages;
 
 foreach my $cust_pkg ( @packages ) {
   $cust_pkg->{'_cust_pkg_discount_active'} =
diff --git a/httemplate/view/cust_main/packages/package.html b/httemplate/view/cust_main/packages/package.html
index 4b56e6f..8aa6403 100644
--- a/httemplate/view/cust_main/packages/package.html
+++ b/httemplate/view/cust_main/packages/package.html
@@ -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}
@@ -107,7 +106,7 @@
 %              ) {
               ( <%pkg_event_link($cust_pkg)%> )
 %           }
-%         } #!$supplemental
+%         } # a canceled recurring package, or else no_links is in effect
 
         </FONT>
       </TD>
@@ -297,6 +296,7 @@
   </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+)$/;
diff --git a/httemplate/view/cust_main/packages/section.html b/httemplate/view/cust_main/packages/section.html
index fe9f283..490f09c 100755
--- a/httemplate/view/cust_main/packages/section.html
+++ b/httemplate/view/cust_main/packages/section.html
@@ -71,9 +71,11 @@
     <& .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>
diff --git a/httemplate/view/cust_main/packages/status.html b/httemplate/view/cust_main/packages/status.html
index 81156c9..7e125f7 100644
--- a/httemplate/view/cust_main/packages/status.html
+++ b/httemplate/view/cust_main/packages/status.html
@@ -44,11 +44,11 @@
       </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 ) %>
 
@@ -97,7 +97,10 @@
     <% 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>
@@ -203,7 +206,7 @@
 
           <% 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
@@ -259,7 +262,11 @@
       <% 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}%>>
@@ -277,27 +284,29 @@
 %               }
 %           }
 
+%           if ( !$supplemental ) {
 %           # suspension actions--always available
-%           if ( $curuser->access_right('Suspend customer package') ) { 
-              ( <% pkg_suspend_link($cust_pkg) %> )
-%           } 
-%           if ( $curuser->access_right('Suspend customer package later') ) { 
-              ( <% pkg_adjourn_link($cust_pkg) %> )
-%           } 
-%           if ( $curuser->access_right('Delay suspension events') ) { 
-              ( <% pkg_delay_link($cust_pkg) %> )
-%           }
+%             if ( $curuser->access_right('Suspend customer package') ) {
+                ( <% pkg_suspend_link($cust_pkg) %> )
+%             }
+%             if ( $curuser->access_right('Suspend customer package later') ) {
+                ( <% pkg_adjourn_link($cust_pkg) %> )
+%             }
+%             if ( $curuser->access_right('Delay suspension events') ) { 
+                ( <% pkg_delay_link($cust_pkg) %> )
+%             }
 %
-%           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') ) { 
-                ( <% pkg_cancel_link($cust_pkg) %> )
+%             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') ) {
+                  ( <% pkg_cancel_link($cust_pkg) %> )
+%               }
+%               if ( $curuser->access_right('Cancel customer package later') ) {
+                  ( <% pkg_expire_link($cust_pkg) %> )
+%               }
 %             }
-%             if ( $curuser->access_right('Cancel customer package later') ) { 
-                ( <% pkg_expire_link($cust_pkg) %> )
-%             } 
 %           }
 
             <FONT>

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/Schema.pm                                  |    8 ++
 FS/FS/cust_main/Billing.pm                       |   34 ++++--
 FS/FS/cust_pkg.pm                                |  139 ++++++++++++++++++----
 FS/FS/part_pkg.pm                                |   43 +++++++
 FS/FS/part_pkg/flat.pm                           |   12 --
 FS/FS/part_pkg_link.pm                           |   10 +-
 httemplate/browse/part_pkg.cgi                   |   72 ++++++++---
 httemplate/edit/part_pkg.cgi                     |   47 ++++++--
 httemplate/elements/freeside.css                 |   12 ++
 httemplate/elements/select.html                  |   26 ++++
 httemplate/elements/tr-select-expire_months.html |   10 ++
 httemplate/elements/tr-select-months.html        |   12 ++
 httemplate/view/cust_main/packages.html          |   26 +++-
 httemplate/view/cust_main/packages/package.html  |   24 ++--
 httemplate/view/cust_main/packages/section.html  |    8 +-
 httemplate/view/cust_main/packages/status.html   |   57 +++++----
 16 files changed, 418 insertions(+), 122 deletions(-)
 create mode 100644 httemplate/elements/tr-select-expire_months.html
 create mode 100644 httemplate/elements/tr-select-months.html




More information about the freeside-commits mailing list