package start_on_hold flag, and better behavior for automatic timers + packages on...
authorMark Wells <mark@freeside.biz>
Tue, 16 Sep 2014 21:54:44 +0000 (14:54 -0700)
committerMark Wells <mark@freeside.biz>
Tue, 16 Sep 2014 21:54:49 +0000 (14:54 -0700)
FS/FS/Schema.pm
FS/FS/cust_pkg.pm
FS/FS/part_pkg.pm
FS/FS/part_pkg/flat.pm
httemplate/edit/part_pkg.cgi
httemplate/elements/order_pkg.js
httemplate/elements/tr-select-cust-part_pkg.html
httemplate/misc/cust-part_pkg.cgi

index 1de3709..04e5dd4 100644 (file)
@@ -3052,6 +3052,7 @@ sub tables_hashref {
         'successor',     'int',     'NULL', '', '', '',
         'family_pkgpart','int',     'NULL', '', '', '',
         'delay_start',   'int',     'NULL', '', '', '',
+        'start_on_hold', 'char',    'NULL',  1, '', '',
         'agent_pkgpartid', 'varchar', 'NULL', 20, '', '',
       ],
       'primary_key'  => 'pkgpart',
index 174d5cf..6d3ed2e 100644 (file)
@@ -241,6 +241,39 @@ sub cust_unlinked_msg {
   ' (cust_pkg.pkgnum '. $self->pkgnum. ')';
 }
 
+=item set_initial_timers
+
+If required by the package definition, sets any automatic expire, adjourn,
+or contract_end timers to some number of months after the start date 
+(or setup date, if the package has already been setup). If the package has
+a delayed setup fee after a period of "free days", will also set the 
+start date to the end of that period.
+
+=cut
+
+sub set_initial_timers {
+  my $self = shift;
+  my $part_pkg = $self->part_pkg;
+  foreach my $action ( qw(expire adjourn contract_end) ) {
+    my $months = $part_pkg->option("${action}_months",1);
+    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 "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)
+  if ( $part_pkg->option('free_days',1)
+       && $part_pkg->option('delay_setup',1)
+     )
+  {
+    $self->start_date( $part_pkg->default_start_date );
+  }
+  '';
+}
+
 =item insert [ OPTION => VALUE ... ]
 
 Adds this billing item to the database ("Orders" the item).  If there is an
@@ -305,6 +338,9 @@ sub insert {
 
   if ( ! $import && ! $options{'change'} ) {
 
+    # set order date to now
+    $self->order_date(time) unless ($import && $self->order_date);
+
     # if the package def says to start only on the first of the month:
     if ( $part_pkg->option('start_1st', 1) && !$self->start_date ) {
       my ($sec,$min,$hour,$mday,$mon,$year) = (localtime(time) )[0,1,2,3,4,5];
@@ -313,35 +349,17 @@ sub insert {
       $self->start_date( timelocal_nocheck(0,0,0,1,$mon,$year) );
     }
 
-    # set up any automatic expire/adjourn/contract_end timers
-    # based on the start date
-    foreach my $action ( qw(expire adjourn contract_end) ) {
-      my $months = $part_pkg->option("${action}_months",1);
-      if($months and !$self->$action) {
-        my $start = $self->start_date || $self->setup || time;
-        $self->$action( $part_pkg->add_freq($start, $months) );
-      }
-    }
-
-    # 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)
-    if (    ! $options{'change'}
-         && $part_pkg->option('free_days', 1)
-         && $part_pkg->option('delay_setup',1)
-         #&& ! $self->start_date
-       )
-    {
-      $self->start_date( $part_pkg->default_start_date );
+    if ($self->susp eq 'now' or $part_pkg->start_on_hold) {
+      # if the package was ordered on hold:
+      # - suspend it
+      # - don't set the start date (it will be started manually)
+      $self->set('susp', $self->order_date);
+      $self->set('start_date', '');
+    } else {
+      # set expire/adjourn/contract_end timers, and free days, if appropriate
+      $self->set_initial_timers;
     }
-  }
-
-  # set order date unless it was specified as part of an import
-  # or this was previously a different package
-  $self->order_date(time) unless ($import && $self->order_date)
-                              or $self->change_pkgnum;
-
-  $self->susp( $self->order_date ) if $self->susp eq 'now';
+  } # else this is a package change, and shouldn't have "new package" behavior
 
   my $oldAutoCommit = $FS::UID::AutoCommit;
   local $FS::UID::AutoCommit = 0;
@@ -1470,6 +1488,8 @@ sub unsuspend {
     return "";  # no error                     # complain instead?
   }
 
+  # handle the case of setting a future unsuspend (resume) date
+  # and do not continue to actually unsuspend the package
   my $date = $opt{'date'};
   if ( $date and $date > time ) { # return an error if $date <= time?
 
@@ -1493,6 +1513,11 @@ sub unsuspend {
   
   } #if $date 
 
+  if (!$self->setup) {
+    # then this package is being released from on-hold status
+    $self->set_initial_timers;
+  }
+
   my @labels = ();
 
   foreach my $cust_svc (
index 77f70e4..59bc051 100644 (file)
@@ -122,6 +122,10 @@ part_pkg, will be equal to pkgpart.
 
 =item delay_start - Number of days to delay package start, by default
 
+=item start_on_hold - 'Y' to suspend this package immediately when it is 
+ordered. The package will not start billing or have a setup fee charged 
+until it is manually unsuspended.
+
 =back
 
 =head1 METHODS
@@ -672,14 +676,15 @@ sub check {
     || $self->ut_textn('comment')
     || $self->ut_textn('promo_code')
     || $self->ut_alphan('plan')
-    || $self->ut_enum('setuptax', [ '', 'Y' ] )
-    || $self->ut_enum('recurtax', [ '', 'Y' ] )
+    || $self->ut_flag('setuptax')
+    || $self->ut_flag('recurtax')
     || $self->ut_textn('taxclass')
-    || $self->ut_enum('disabled', [ '', 'Y' ] )
-    || $self->ut_enum('custom', [ '', 'Y' ] )
-    || $self->ut_enum('no_auto', [ '', 'Y' ])
-    || $self->ut_enum('recur_show_zero', [ '', 'Y' ])
-    || $self->ut_enum('setup_show_zero', [ '', 'Y' ])
+    || $self->ut_flag('disabled')
+    || $self->ut_flag('custom')
+    || $self->ut_flag('no_auto')
+    || $self->ut_flag('recur_show_zero')
+    || $self->ut_flag('setup_show_zero')
+    || $self->ut_flag('start_on_hold')
     #|| $self->ut_moneyn('setup_cost')
     #|| $self->ut_moneyn('recur_cost')
     || $self->ut_floatn('setup_cost')
@@ -1130,7 +1135,10 @@ sub is_free {
 sub can_discount { 0; }
  
 # whether the plan allows changing the start date
-sub can_start_date { 1; }
+sub can_start_date {
+  my $self = shift;
+  $self->start_on_hold ? 0 : 1;
+}
 
 # whether the plan supports part_pkg_usageprice add-ons (a specific kind of
 #  pre-selectable usage pricing, there's others this doesn't refer to)
index 1594a13..cb2986e 100644 (file)
@@ -272,6 +272,7 @@ sub is_prepaid { 0; } #no, we're postpaid
 sub can_start_date {
   my $self = shift;
   my %opt = @_;
+  return 0 if $self->start_on_hold;
 
   ! $self->option('start_1st', 1) && (   ! $self->option('sync_bill_date',1)
                                       || ! $self->option('prorate_defer_bill',1)
index 65eca6c..cc5606e 100755 (executable)
@@ -44,6 +44,7 @@
                    'plan'             => 'Price plan',
                    'disabled'         => 'Disable new orders',
                    'disable_line_item_date_ranges' => 'Disable line item date ranges',
+                   'start_on_hold'    => 'Start on hold',
                    'setup_cost'       => 'Setup cost',
                    'recur_cost'       => 'Recur cost',
                    'pay_weight'       => 'Payment weight',
                      ),
                      {field=>'disabled', type=>$disabled_type, value=>'Y'},
                      {field=>'disable_line_item_date_ranges', type=>$disabled_type, value=>'Y'},
+                     { field => 'start_on_hold',
+                       type => 'checkbox',
+                       value => 'Y'
+                     },
 
                      { type     => 'tablebreak-tr-title',
                        value    => 'Pricing', #better name?
index a145cbb..3586a54 100644 (file)
@@ -10,7 +10,7 @@ function pkg_changed () {
     var date_text = document.getElementById('start_date_text');
 
     var radio_now = document.getElementById('start_now');
-    //var radio_on_hold = document.getElementById('start_on_hold');
+    var radio_on_hold = document.getElementById('start_on_hold');
     var radio_on_date = document.getElementById('start_on_date');
 
     form.submitButton.disabled = false;
@@ -36,23 +36,35 @@ function pkg_changed () {
       date_button.style.display = '';
       date_button_disabled.style.display = 'none';
       if ( radio_on_date ) {
+        // un-disable all the buttons that might get disabled
         radio_on_date.disabled = false;
-        if ( form.start_date_text.value.length > 0 && radio_now.checked ) {
+        radio_now.disabled = false;
+        // if a start date has been entered, assume the user wants it
+        if ( form.start_date_text.value.length > 0 ) {
           radio_now.checked = false;
           radio_on_date.checked = true;
+        } else {
+          // if not, default to now
+          radio_now.checked = true;
         }
       }
-    } else {
+    } else { // the package is either fixed start date or start-on-hold
       date_text.style.backgroundColor = '#dddddd';
       date_text.disabled = true;
       date_button.style.display = 'none';
       date_button_disabled.style.display = '';
       if ( radio_on_date ) {
-        if ( radio_on_date.checked ) {
-          radio_on_date.checked = false;
+        if ( opt.getAttribute('data-start_on_hold') == 1 ) {
+          // disallow all options but "On hold"
+          radio_on_hold.checked = true;
+          radio_now.checked = false;
+          radio_now.disabled = true;
+        } else {
+          // disallow all options but "On date"
+          radio_on_hold.checked = false;
           radio_now.checked = true;
+          radio_now.disabled = false;
         }
-        radio_on_date.disabled = true;
       }
     }
 
index 696baff..0db989a 100644 (file)
@@ -5,9 +5,10 @@
 
   <SCRIPT TYPE="text/javascript">
 
-    function part_pkg_opt(what, value, text, can_discount, can_start_date, start_date) {
+    function part_pkg_opt(what, value, text, can_discount, start_on_hold, can_start_date, start_date) {
       var optionName = new Option(text, value, false, false);
       optionName.setAttribute('data-can_discount',   can_discount);
+      optionName.setAttribute('data-start_on_hold',  start_on_hold);
       optionName.setAttribute('data-can_start_date', can_start_date);
       optionName.setAttribute('data-start_date',     start_date || '');
       var length = what.length;
         // add the new packages
         opt(what.form.pkgpart, '', 'Select package');
         var packagesArray = eval('(' + part_pkg + ')' );
-        for ( var s = 0; s < packagesArray.length; s=s+5 ) {
+        while (packagesArray.length > 0) {
           //surely this should be some kind of JSON structure
-          var packagesLabel  = packagesArray[s+1];
-          var can_discount   = packagesArray[s+2];
-          var can_start_date = packagesArray[s+3];
-          var start_date     = packagesArray[s+4];
+          var pkgpart        = packagesArray.shift();
+          var label          = packagesArray.shift();
+          var can_discount   = packagesArray.shift();
+          var start_on_hold  = packagesArray.shift();
+          var can_start_date = packagesArray.shift();
+          var start_date     = packagesArray.shift();
           part_pkg_opt(
-            what.form.pkgpart, packagesArray[s], packagesLabel, can_discount, can_start_date, start_date
+            what.form.pkgpart, pkgpart, label, can_discount, start_on_hold, can_start_date, start_date
           );
         }
 
index e129347..dc9ba2a 100644 (file)
@@ -56,6 +56,7 @@ my @return = map  {
                     ( $_->pkgpart,
                       $_->pkg_comment,
                       $_->can_discount,
+                      ($_->start_on_hold ? 1 : 0),
                       $_->can_start_date(
                         num_ncancelled_pkgs => $num_ncancelled_pkgs,
                       ),