RT#18361: Delay package from billing until services are provisioned
authorJonathan Prykop <jonathan@freeside.biz>
Fri, 31 Jul 2015 06:33:11 +0000 (01:33 -0500)
committerJonathan Prykop <jonathan@freeside.biz>
Fri, 31 Jul 2015 06:33:11 +0000 (01:33 -0500)
FS/FS/Schema.pm
FS/FS/cust_pkg.pm
FS/FS/cust_svc.pm
FS/FS/part_pkg.pm
FS/FS/pkg_svc.pm
httemplate/edit/process/part_pkg.cgi
httemplate/elements/tr-pkg_svc.html

index c8b9b63..184c6c9 100644 (file)
@@ -3543,6 +3543,7 @@ sub tables_hashref {
         'primary_svc',   'char', 'NULL', 1, '', '', 
         'hidden',        'char', 'NULL', 1, '', '',
         'bulk_skip',     'char', 'NULL', 1, '', '',
+        'provision_hold', 'char', 'NULL', 1, '', '',
       ],
       'primary_key'  => 'pkgsvcnum',
       'unique'       => [ ['pkgpart', 'svcpart'] ],
index fbecd8d..c5a3d2e 100644 (file)
@@ -3389,28 +3389,33 @@ Returns a list of FS::part_svc objects representing services included in this
 package but not yet provisioned.  Each FS::part_svc object also has an extra
 field, I<num_avail>, which specifies the number of available services.
 
+Accepts option I<provision_hold>;  if true, only returns part_svc for which the
+associated pkg_svc has the provision_hold flag set.
+
 =cut
 
 sub available_part_svc {
   my $self = shift;
+  my %opt  = @_;
 
   my $pkg_quantity = $self->quantity || 1;
 
   grep { $_->num_avail > 0 }
-    map {
-          my $part_svc = $_->part_svc;
-          $part_svc->{'Hash'}{'num_avail'} = #evil encapsulation-breaking
-            $pkg_quantity * $_->quantity - $self->num_cust_svc($_->svcpart);
-
-         # more evil encapsulation breakage
-         if($part_svc->{'Hash'}{'num_avail'} > 0) {
-           my @exports = $part_svc->part_export_did;
-           $part_svc->{'Hash'}{'can_get_dids'} = scalar(@exports);
-         }
-
-          $part_svc;
-        }
-      $self->part_pkg->pkg_svc;
+  map {
+    my $part_svc = $_->part_svc;
+    $part_svc->{'Hash'}{'num_avail'} = #evil encapsulation-breaking
+    $pkg_quantity * $_->quantity - $self->num_cust_svc($_->svcpart);
+
+    # more evil encapsulation breakage
+    if ($part_svc->{'Hash'}{'num_avail'} > 0) {
+      my @exports = $part_svc->part_export_did;
+      $part_svc->{'Hash'}{'can_get_dids'} = scalar(@exports);
+       }
+
+    $part_svc;
+  }
+  grep { $opt{'provision_hold'} ? $_->provision_hold : 1 }
+  $self->part_pkg->pkg_svc;
 }
 
 =item part_svc [ OPTION => VALUE ... ]
index 986c5ae..c5099fc 100644 (file)
@@ -102,6 +102,37 @@ sub table { 'cust_svc'; }
 Adds this service to the database.  If there is an error, returns the error,
 otherwise returns false.
 
+=cut
+
+sub insert {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::insert;
+
+  #check if this releases a hold (see FS::pkg_svc provision_hold)
+  $error ||= $self->_provision_hold;
+
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error if $error
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  ''; #no error
+
+}
+
 =item delete
 
 Deletes this service from the database.  If there is an error, returns the
@@ -428,6 +459,9 @@ sub replace {
     } # if ($svc_x->locationnum)
   } # if this is a location change
 
+  #check if this releases a hold (see FS::pkg_svc provision_hold)
+  $error ||= $new->_provision_hold;
+
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error if $error
@@ -1206,6 +1240,35 @@ sub smart_search_param {
   );
 }
 
+# If the associated cust_pkg is 'on hold'
+# and the associated pkg_svc has the provision_hold flag
+# and there are no more available_part_svcs on the cust_pkg similarly flagged,
+# then removes hold from pkg
+# returns $error or '' on success,
+# does not indicate if pkg status was changed
+sub _provision_hold {
+  my $self = shift;
+
+  # check status of cust_pkg
+  my $cust_pkg = $self->cust_pkg;
+  return '' unless $cust_pkg->status eq 'on hold';
+
+  # check flag on this svc
+  # small false laziness with $self->pkg_svc
+  # to avoid looking up cust_pkg twice
+  my $pkg_svc  = qsearchs( 'pkg_svc', {
+    'svcpart' => $self->svcpart,
+    'pkgpart' => $cust_pkg->pkgpart,
+  });
+  return '' unless $pkg_svc->provision_hold;
+
+  # check for any others available with that flag
+  return '' if $cust_pkg->available_part_svc( 'provision_hold' => 1 );
+
+  # conditions met, remove hold
+  return $cust_pkg->unsuspend;
+}
+
 sub _upgrade_data {
   my $class = shift;
 
index 498da8a..97bce45 100644 (file)
@@ -183,7 +183,8 @@ I<custnum_ref> and I<options>.
 If I<pkg_svc> is set to a hashref with svcparts as keys and quantities as
 values, appropriate FS::pkg_svc records will be inserted.  I<hidden_svc> can 
 be set to a hashref of svcparts and flag values ('Y' or '') to set the 
-'hidden' field in these records.
+'hidden' field in these records, and I<provision_hold> can be set similarly
+for the 'provision_hold' field in these records.
 
 If I<primary_svc> is set to the svcpart of the primary service, the appropriate
 FS::pkg_svc record will be updated.
@@ -293,6 +294,7 @@ sub insert {
     warn "  inserting pkg_svc records" if $DEBUG;
     my $pkg_svc = $options{'pkg_svc'} || {};
     my $hidden_svc = $options{'hidden_svc'} || {};
+    my $provision_hold = $options{'provision_hold'} || {};
     foreach my $part_svc ( qsearch('part_svc', {} ) ) {
       my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
       my $primary_svc =
@@ -306,6 +308,7 @@ sub insert {
         'quantity'    => $quantity, 
         'primary_svc' => $primary_svc,
         'hidden'      => $hidden_svc->{$part_svc->svcpart},
+        'provision_hold' => $provision_hold->{$part_svc->svcpart},
       } );
       my $error = $pkg_svc->insert;
       if ( $error ) {
@@ -386,15 +389,15 @@ sub delete {
 Replaces OLD_RECORD with this one in the database.  If there is an error,
 returns the error, otherwise returns false.
 
-Currently available options are: I<pkg_svc>, I<hidden_svc>, I<primary_svc> 
-and I<options>
+Currently available options are: I<pkg_svc>, I<hidden_svc>, I<primary_svc>,
+I<bulk_skip>, I<provision_hold> and I<options>
 
 If I<pkg_svc> is set to a hashref with svcparts as keys and quantities as
 values, the appropriate FS::pkg_svc records will be replaced.  I<hidden_svc>
 can be set to a hashref of svcparts and flag values ('Y' or '') to set the 
-'hidden' field in these records.  I<bulk_skip> can be set to a hashref of
-svcparts and flag values ('Y' or '') to set the 'bulk_skip' field in those
-records.
+'hidden' field in these records.  I<bulk_skip> and I<provision_hold> can be set 
+to a hashref of svcparts and flag values ('Y' or '') to set the respective field 
+in those records.
 
 If I<primary_svc> is set to the svcpart of the primary service, the appropriate
 FS::pkg_svc record will be updated.
@@ -532,12 +535,14 @@ sub replace {
   my $pkg_svc = $options->{'pkg_svc'};
   my $hidden_svc = $options->{'hidden_svc'} || {};
   my $bulk_skip  = $options->{'bulk_skip'} || {};
+  my $provision_hold = $options->{'provision_hold'} || {};
   if ( $pkg_svc ) { # if it wasn't passed, don't change existing pkg_svcs
 
     foreach my $part_svc ( qsearch('part_svc', {} ) ) {
       my $quantity  = $pkg_svc->{$part_svc->svcpart} || 0;
       my $hidden    = $hidden_svc->{$part_svc->svcpart} || '';
       my $bulk_skip = $bulk_skip->{$part_svc->svcpart} || '';
+      my $provision_hold = $provision_hold->{$part_svc->svcpart} || '';
       my $primary_svc =
         ( defined($options->{'primary_svc'}) && $options->{'primary_svc'}
           && $options->{'primary_svc'} == $part_svc->svcpart
@@ -554,18 +559,21 @@ sub replace {
       my $old_primary_svc = '';
       my $old_hidden = '';
       my $old_bulk_skip = '';
+      my $old_provision_hold = '';
       if ( $old_pkg_svc ) {
         $old_quantity = $old_pkg_svc->quantity;
         $old_primary_svc = $old_pkg_svc->primary_svc 
           if $old_pkg_svc->dbdef_table->column('primary_svc'); # is this needed?
         $old_hidden = $old_pkg_svc->hidden;
-        $old_bulk_skip = $old_pkg_svc->old_bulk_skip;
+        $old_bulk_skip = $old_pkg_svc->old_bulk_skip; # should this just be bulk_skip?
+        $old_provision_hold = $old_pkg_svc->provision_hold;
       }
    
       next unless $old_quantity    != $quantity
                || $old_primary_svc ne $primary_svc
                || $old_hidden      ne $hidden
-               || $old_bulk_skip   ne $bulk_skip;
+               || $old_bulk_skip   ne $bulk_skip
+               || $old_provision_hold ne $provision_hold;
     
       my $new_pkg_svc = new FS::pkg_svc( {
         'pkgsvcnum'   => ( $old_pkg_svc ? $old_pkg_svc->pkgsvcnum : '' ),
@@ -575,6 +583,7 @@ sub replace {
         'primary_svc' => $primary_svc,
         'hidden'      => $hidden,
         'bulk_skip'   => $bulk_skip,
+        'provision_hold' => $provision_hold,
       } );
       my $error = $old_pkg_svc
                     ? $new_pkg_svc->replace($old_pkg_svc)
index 4efffd9..b2dc870 100644 (file)
@@ -47,6 +47,8 @@ definition includes
 
 =item hidden - 'Y' to hide this service on invoices, null otherwise.
 
+=item provision_hold - 'Y' to release package hold when all services marked with this are provisioned
+
 =back
 
 =head1 METHODS
@@ -107,6 +109,7 @@ sub check {
     || $self->ut_number('svcpart')
     || $self->ut_number('quantity')
     || $self->ut_enum('hidden', [ '', 'Y' ] )
+    || $self->ut_flag('provision_hold')
   ;
   return $error if $error;
 
index 61a6337..b804202 100755 (executable)
@@ -160,6 +160,7 @@ my $args_callback = sub {
   my @svcparts = map { $_->svcpart } qsearch('part_svc', {});
   my %pkg_svc    = map { $_ => scalar($cgi->param("pkg_svc$_"  )) } @svcparts;
   my %hidden_svc = map { $_ => scalar($cgi->param("hidden$_"   )) } @svcparts;
+  my %provision_hold = map { $_ => scalar($cgi->param("provision_hold$_"   )) } @svcparts;
   my %bulk_skip  = map { $_ => ( $cgi->param("no_bulk_skip$_") eq 'Y'
                                    ? '' : 'Y'
                                )
@@ -167,6 +168,7 @@ my $args_callback = sub {
 
   push @args, 'pkg_svc'    => \%pkg_svc,
               'hidden_svc' => \%hidden_svc,
+              'provision_hold' => \%provision_hold,
               'bulk_skip'  => \%bulk_skip;
 
   ###
index a44c5b9..cfef51c 100644 (file)
 %  } else {
 %    $bulk_skip = $pkg_svc->bulk_skip;
 %  }
+%  my $provision_hold = '';
+%  if ( grep { $_ eq "provision_hold$svcpart" } $cgi->param ) {
+%    $provision_hold = $cgi->param("hidden_svc$svcpart");
+%  } else {
+%    $provision_hold = $pkg_svc->provision_hold;
+%  }
 %
 %  my @exports = $pkg_svc->part_svc->part_export;
 %  foreach my $export ( @exports ) {
       <INPUT TYPE="checkbox" NAME="no_bulk_skip<% $svcpart %>" VALUE="Y"<% $bulk_skip =~ /^Y/i ? '' : ' CHECKED' %>>
     </TD>
 
+    <TD ALIGN="center">
+      <INPUT TYPE="checkbox" NAME="provision_hold<% $svcpart %>" VALUE="Y"<% $provision_hold =~ /^Y/i ? ' CHECKED' : ''%>>
+    </TD>
+
   </TR>
 % foreach ( 1 .. $columns-1 ) {
 %       if ( $count == int( $_ * scalar(@part_svc) / $columns ) ) { 
@@ -127,6 +137,7 @@ my $thead =  "\n\n". ntable('#cccccc', 2).
              '<TH BGCOLOR="#dcdcdc">Service</TH>'.
              '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Hide<BR>from<BR>Invoices</FONT></TH>'.
              '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Bulk<BR>Charge</FONT></TH>'.
+             '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Hold<BR>Until<BR>Provision</FONT></TH>'.
              '</TR>';
 
 my $part_pkg = $opt{'object'};