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, '', '',
         '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'] ],
       ],
       '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.
 
 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;
 =cut
 
 sub available_part_svc {
   my $self = shift;
+  my %opt  = @_;
 
   my $pkg_quantity = $self->quantity || 1;
 
   grep { $_->num_avail > 0 }
 
   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 ... ]
 }
 
 =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.
 
 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
 =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
 
     } # 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
   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;
 
 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 
 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.
 
 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'} || {};
     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 =
     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},
         '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 ) {
       } );
       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.
 
 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 
 
 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.
 
 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 $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} || '';
   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
       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_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;
       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
       }
    
       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 : '' ),
     
       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,
         '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)
       } );
       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 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
 =back
 
 =head1 METHODS
@@ -107,6 +109,7 @@ sub check {
     || $self->ut_number('svcpart')
     || $self->ut_number('quantity')
     || $self->ut_enum('hidden', [ '', 'Y' ] )
     || $self->ut_number('svcpart')
     || $self->ut_number('quantity')
     || $self->ut_enum('hidden', [ '', 'Y' ] )
+    || $self->ut_flag('provision_hold')
   ;
   return $error if $error;
 
   ;
   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 @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'
                                )
   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,
 
   push @args, 'pkg_svc'    => \%pkg_svc,
               'hidden_svc' => \%hidden_svc,
+              'provision_hold' => \%provision_hold,
               'bulk_skip'  => \%bulk_skip;
 
   ###
               'bulk_skip'  => \%bulk_skip;
 
   ###
index a44c5b9..cfef51c 100644 (file)
 %  } else {
 %    $bulk_skip = $pkg_svc->bulk_skip;
 %  }
 %  } 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 ) {
 %
 %  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>
 
       <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 ) ) { 
   </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">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'};
              '</TR>';
 
 my $part_pkg = $opt{'object'};