RT#18361 Delay package from billing until services are provisioned [v3 backport]
authorJonathan Prykop <jonathan@freeside.biz>
Fri, 31 Jul 2015 06:33:11 +0000 (01:33 -0500)
committerJonathan Prykop <jonathan@freeside.biz>
Fri, 7 Aug 2015 04:56:14 +0000 (23:56 -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 3d7248f..e06fce6 100644 (file)
@@ -2405,6 +2405,7 @@ sub tables_hashref {
         'quantity',   'int',    '',   '', '', '', 
         'primary_svc','char', 'NULL',  1, '', '', 
         'hidden',     'char', 'NULL',  1, '', '',
         'quantity',   'int',    '',   '', '', '', 
         'primary_svc','char', 'NULL',  1, '', '', 
         'hidden',     'char', 'NULL',  1, '', '',
+        'provision_hold', 'char', 'NULL', 1, '', '',
       ],
       'primary_key' => 'pkgsvcnum',
       'unique' => [ ['pkgpart', 'svcpart'] ],
       ],
       'primary_key' => 'pkgsvcnum',
       'unique' => [ ['pkgpart', 'svcpart'] ],
index 0b5a07f..1d660ee 100644 (file)
@@ -3308,28 +3308,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 12e2c78..a1df059 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
@@ -360,6 +391,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
@@ -1073,6 +1107,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 18a065d..402d88c 100644 (file)
@@ -168,7 +168,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.
@@ -248,6 +249,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 =
@@ -261,6 +263,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 ) {
@@ -335,13 +338,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.
+'hidden' field in these records.  I<provision_hold> can be set 
+to a hashref of svcparts and flag values ('Y' or '') to set the 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.
@@ -447,10 +452,12 @@ sub replace {
   warn "  replacing pkg_svc records" if $DEBUG;
   my $pkg_svc = $options->{'pkg_svc'};
   my $hidden_svc = $options->{'hidden_svc'} || {};
   warn "  replacing pkg_svc records" if $DEBUG;
   my $pkg_svc = $options->{'pkg_svc'};
   my $hidden_svc = $options->{'hidden_svc'} || {};
+  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} || '';
   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 $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
@@ -466,16 +473,19 @@ sub replace {
       my $old_quantity = 0;
       my $old_primary_svc = '';
       my $old_hidden = '';
       my $old_quantity = 0;
       my $old_primary_svc = '';
       my $old_hidden = '';
+      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_provision_hold = $old_pkg_svc->provision_hold;
       }
    
       next unless $old_quantity != $quantity || 
                   $old_primary_svc ne $primary_svc ||
       }
    
       next unless $old_quantity != $quantity || 
                   $old_primary_svc ne $primary_svc ||
-                  $old_hidden ne $hidden;
+                  $old_hidden ne $hidden ||
+                  $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 : '' ),
@@ -484,6 +494,7 @@ sub replace {
         'quantity'    => $quantity, 
         'primary_svc' => $primary_svc,
         'hidden'      => $hidden,
         'quantity'    => $quantity, 
         'primary_svc' => $primary_svc,
         'hidden'      => $hidden,
+        '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 f79bb5e..fa82ec0 100644 (file)
@@ -52,6 +52,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
@@ -112,6 +114,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 8e8be85..3ffd5fc 100755 (executable)
@@ -128,8 +128,11 @@ 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;
 
 
-  push @args, 'pkg_svc' => \%pkg_svc, 'hidden_svc' => \%hidden_svc;
+  push @args, 'pkg_svc'    => \%pkg_svc,
+              'hidden_svc' => \%hidden_svc,
+              'provision_hold' => \%provision_hold;
 
   ###
   # cust_pkg and custnum_ref (inserts only)
 
   ###
   # cust_pkg and custnum_ref (inserts only)
index 8acbca1..b3bf802 100644 (file)
 %    $quan = $pkg_svc->quantity;
 %  }
 %
 %    $quan = $pkg_svc->quantity;
 %  }
 %
+%  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 ) {
 %      push @possible_exports, $export if $export->can('external_pkg_map');
 %  my @exports = $pkg_svc->part_svc->part_export;
 %  foreach my $export ( @exports ) {
 %      push @possible_exports, $export if $export->can('external_pkg_map');
     <TD>
       <INPUT TYPE="checkbox" NAME="hidden<% $svcpart %>" VALUE="Y"<% $pkg_svc->hidden =~ /^Y/i ? ' CHECKED' : ''%>>
     </TD>
     <TD>
       <INPUT TYPE="checkbox" NAME="hidden<% $svcpart %>" VALUE="Y"<% $pkg_svc->hidden =~ /^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 ) ) { 
@@ -106,6 +118,7 @@ my $thead =  "\n\n". ntable('#cccccc', 2).
              '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-2>Primary</FONT></TH>'.
              '<TH BGCOLOR="#dcdcdc">Service</TH>'.
              '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Hide</FONT></TH>'.
              '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-2>Primary</FONT></TH>'.
              '<TH BGCOLOR="#dcdcdc">Service</TH>'.
              '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Hide</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'};