backup the schema for tables we don't need the data from. RT#85959
[freeside.git] / FS / FS / cust_svc.pm
index f499bef..cc5f4bf 100644 (file)
@@ -3,7 +3,7 @@ use base qw( FS::cust_main_Mixin FS::option_Common ); #FS::Record );
 
 use strict;
 use vars qw( $DEBUG $me $ignore_quantity $conf $ticket_system );
 
 use strict;
 use vars qw( $DEBUG $me $ignore_quantity $conf $ticket_system );
-use Carp;
+use Carp qw(cluck);
 #use Scalar::Util qw( blessed );
 use List::Util qw( max );
 use FS::Conf;
 #use Scalar::Util qw( blessed );
 use List::Util qw( max );
 use FS::Conf;
@@ -11,10 +11,13 @@ use FS::Record qw( qsearch qsearchs dbh str2time_sql str2time_sql_closing );
 use FS::part_pkg;
 use FS::part_svc;
 use FS::pkg_svc;
 use FS::part_pkg;
 use FS::part_svc;
 use FS::pkg_svc;
+use FS::part_svc_link;
 use FS::domain_record;
 use FS::part_export;
 use FS::cdr;
 use FS::UI::Web;
 use FS::domain_record;
 use FS::part_export;
 use FS::cdr;
 use FS::UI::Web;
+use FS::export_cust_svc;
+use FS::DBI;
 
 #most FS::svc_ classes are autoloaded in svc_x emthod
 use FS::svc_acct;  #this one is used in the cache stuff
 
 #most FS::svc_ classes are autoloaded in svc_x emthod
 use FS::svc_acct;  #this one is used in the cache stuff
@@ -31,6 +34,15 @@ FS::UID->install_callback( sub {
   $ticket_system = $conf->config('ticket_system')
 });
 
   $ticket_system = $conf->config('ticket_system')
 });
 
+our $cache_enabled = 0;
+
+sub _simplecache {
+  my( $self, $hashref ) = @_;
+  if ( $cache_enabled && $hashref->{'svc'} ) {
+    $self->{'_svcpart'} = FS::part_svc->new($hashref);
+  }
+}
+
 sub _cache {
   my $self = shift;
   my ( $hashref, $cache ) = @_;
 sub _cache {
   my $self = shift;
   my ( $hashref, $cache ) = @_;
@@ -101,6 +113,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->_check_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
@@ -117,8 +160,53 @@ sub delete {
   my $cust_pkg = $self->cust_pkg;
   my $custnum = $cust_pkg->custnum if $cust_pkg;
 
   my $cust_pkg = $self->cust_pkg;
   my $custnum = $cust_pkg->custnum if $cust_pkg;
 
+  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;
+
+  # delete associated export_cust_svc
+  foreach my $export_cust_svc (
+    qsearch('export_cust_svc',{ 'svcnum' => $self->svcnum })
+  ) {
+    my $error = $export_cust_svc->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
   my $error = $self->SUPER::delete;
   my $error = $self->SUPER::delete;
-  return $error if $error;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  foreach my $part_svc_link ( $self->part_svc_link(
+                                link_type   => 'cust_svc_unprovision_cascade',
+                              )
+  ) {
+    foreach my $cust_svc ( qsearch( 'cust_svc', {
+                             'pkgnum'  => $self->pkgnum,
+                             'svcpart' => $part_svc_link->dst_svcpart,
+                           })
+    ) {
+      my $error = $cust_svc->svc_x->delete;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   if ( $ticket_system eq 'RT_Internal' ) {
     unless ( $rt_session ) {
 
   if ( $ticket_system eq 'RT_Internal' ) {
     unless ( $rt_session ) {
@@ -143,6 +231,40 @@ sub delete {
       warn "error unlinking ticket $svcnum: $msg\n" if !$val;
     }
   }
       warn "error unlinking ticket $svcnum: $msg\n" if !$val;
     }
   }
+
+  '';
+
+}
+
+=item suspend
+
+Suspends the relevant service by calling the B<suspend> method of the associated
+FS::svc_XXX object (i.e. an FS::svc_acct object or FS::svc_domain object).
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub suspend {
+  my( $self, %opt ) = @_;
+
+  $self->part_svc->svcdb =~ /^([\w\-]+)$/ or return 'Illegal part_svc.svcdb';
+  my $svcdb = $1;
+  require "FS/$svcdb.pm";
+
+  my $svc = qsearchs( $svcdb, { 'svcnum' => $self->svcnum } )
+    or return '';
+
+  my $error = $svc->suspend;
+  return $error if $error;
+
+  if ( $opt{labels_arryref} ) {
+    my( $label, $value ) = $self->label;
+    push @{ $opt{labels_arrayref} }, "$label: $value";
+  }
+
+  '';
+
 }
 
 =item cancel
 }
 
 =item cancel
@@ -332,17 +454,41 @@ sub replace {
   #my $error = $new->SUPER::replace($old, @_);
   my $error = $new->SUPER::replace($old);
 
   #my $error = $new->SUPER::replace($old, @_);
   my $error = $new->SUPER::replace($old);
 
-  #trigger a relocate export on location changes
-  if ( $new->cust_pkg->locationnum != $old->cust_pkg->locationnum ) {
-    $error ||= $new->svc_x->export('relocate',
-                                   $new->cust_pkg->cust_location,
-                                   $old->cust_pkg->cust_location,
+  #trigger a relocate export on location changes (NENA2 and Northern 911 export)
+  my $old_pkg = $old->cust_pkg;
+  my $new_pkg = $new->cust_pkg;
+  if ( $old_pkg && $new_pkg && $new_pkg->locationnum != $old_pkg->locationnum ) {
+    my $svc_x = $new->svc_x;
+    if ( $svc_x->locationnum ) {
+      if ( $svc_x->locationnum == $old->cust_pkg->locationnum ) {
+        # in this case, set the service location to be the same as the new
+        # package location
+        $svc_x->set('locationnum', $new->cust_pkg->locationnum);
+        # and replace it, which triggers a relocate export so we don't 
+        # need to
+        $error ||= $svc_x->replace;
+      } else {
+        # the service already has a different location from its package
+        # so don't change it
+      }
+    } else {
+      # the service doesn't have a locationnum (either isn't of a type 
+      # that has the locationnum field, or the locationnum is null and 
+      # defaults to cust_pkg->locationnum)
+      # so just trigger the export here
+      $error ||= $new->svc_x->export('relocate',
+                                     $new->cust_pkg->cust_location,
+                                     $old->cust_pkg->cust_location,
                                   );
                                   );
-  }
+    } # if ($svc_x->locationnum)
+  } # if this is a location change
+
+  #check if this releases a hold (see FS::pkg_svc provision_hold)
+  $error ||= $new->_check_provision_hold;
 
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
 
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
-    return $error if $error;
+    return $error if $error
   }
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   }
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
@@ -412,11 +558,80 @@ sub check {
            " services for pkgnum ". $self->pkgnum
       if $num_avail <= 0;
 
            " services for pkgnum ". $self->pkgnum
       if $num_avail <= 0;
 
+    #part_svc_link rules (only make sense in pkgpart context, and 
+    # skipping this when ignore_quantity is set DTRT when we're "forcing"
+    # an implicit change here (location change triggered pkgpart change, 
+    # ->overlimit, bulk customer service changes)
+    foreach my $part_svc_link ( $self->part_svc_link(
+                                  link_type   => 'cust_svc_provision_restrict',
+                                )
+    ) {
+      return $part_svc_link->dst_svc. ' must be provisioned before '.
+             $part_svc_link->src_svc
+        unless qsearchs({
+          'table'    => 'cust_svc',
+          'hashref'  => { 'pkgnum'  => $self->pkgnum,
+                          'svcpart' => $part_svc_link->dst_svcpart,
+                        },
+          'order_by' => 'LIMIT 1',
+        });
+    }
+
   }
 
   $self->SUPER::check;
 }
 
   }
 
   $self->SUPER::check;
 }
 
+=item check_part_svc_link_unprovision
+
+Checks service dependency unprovision rules for this service.
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub check_part_svc_link_unprovision {
+  my $self = shift;
+
+  foreach my $part_svc_link ( $self->part_svc_link(
+                                link_type   => 'cust_svc_unprovision_restrict',
+                              )
+  ) {
+    return $part_svc_link->dst_svc. ' must be unprovisioned before '.
+           $part_svc_link->src_svc
+      if qsearchs({
+        'table'    => 'cust_svc',
+        'hashref'  => { 'pkgnum'  => $self->pkgnum,
+                        'svcpart' => $part_svc_link->dst_svcpart,
+                      },
+        'order_by' => 'LIMIT 1',
+      });
+  }
+
+  '';
+}
+
+=item part_svc_link
+
+Returns the service dependencies (see L<FS::part_svc_link>) for the given
+search options, taking into account this service definition as source and
+this customer's agent.
+
+Available options are any field in part_svc_link.  Typically used options are
+link_type.
+
+=cut
+
+sub part_svc_link {
+  my $self = shift;
+  my $agentnum = $self->pkgnum ? $self->cust_pkg->cust_main->agentnum : '';
+  FS::part_svc_link->by_agentnum($agentnum,
+    src_svcpart=>$self->svcpart,
+    disabled   => '',
+    @_
+  );
+}
+
 =item display_svcnum 
 
 Returns the displayed service number for this service: agent_svcid if it has a
 =item display_svcnum 
 
 Returns the displayed service number for this service: agent_svcid if it has a
@@ -438,9 +653,9 @@ L<FS::part_svc>).
 
 sub part_svc {
   my $self = shift;
 
 sub part_svc {
   my $self = shift;
-  $self->{'_svcpart'}
-    ? $self->{'_svcpart'}
-    : qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
+  return $self->{_svcpart} if $self->{_svcpart};
+  cluck 'cust_svc->part_svc called' if $DEBUG;
+  qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
 }
 
 =item cust_pkg
 }
 
 =item cust_pkg
@@ -490,10 +705,10 @@ sub pkg_cancel_date {
   return $cust_pkg->getfield('cancel') || '';
 }
 
   return $cust_pkg->getfield('cancel') || '';
 }
 
-=item label
+=item label [ LOCALE ]
 
 Returns a list consisting of:
 
 Returns a list consisting of:
-- The name of this service (from part_svc)
+- The name of this service (from part_svc), optionally localized
 - A meaningful identifier (username, domain, or mail alias)
 - The table name (i.e. svc_domain) for this service
 - svcnum
 - A meaningful identifier (username, domain, or mail alias)
 - The table name (i.e. svc_domain) for this service
 - svcnum
@@ -502,7 +717,7 @@ Usage example:
 
   my($label, $value, $svcdb) = $cust_svc->label;
 
 
   my($label, $value, $svcdb) = $cust_svc->label;
 
-=item label_long
+=item label_long [ LOCALE ]
 
 Like the B<label> method, except the second item in the list ("meaningful
 identifier") may be longer - typically, a full name is included.
 
 Like the B<label> method, except the second item in the list ("meaningful
 identifier") may be longer - typically, a full name is included.
@@ -515,20 +730,25 @@ sub label_long { shift->_label('svc_label_long', @_); }
 sub _label {
   my $self = shift;
   my $method = shift;
 sub _label {
   my $self = shift;
   my $method = shift;
+  my $locale = shift;
   my $svc_x = $self->svc_x
     or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
 
   my $svc_x = $self->svc_x
     or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
 
-  $self->$method($svc_x);
+  $self->$method($svc_x, undef, undef, $locale);
 }
 
 }
 
+# svc_label(_long) takes three arguments: end date, start date, locale
+# and FS::svc_*::label methods must accept those also, if they even care
+
 sub svc_label      { shift->_svc_label('label',      @_); }
 sub svc_label_long { shift->_svc_label('label_long', @_); }
 
 sub _svc_label {
   my( $self, $method, $svc_x ) = ( shift, shift, shift );
 sub svc_label      { shift->_svc_label('label',      @_); }
 sub svc_label_long { shift->_svc_label('label_long', @_); }
 
 sub _svc_label {
   my( $self, $method, $svc_x ) = ( shift, shift, shift );
+  my ($end, $start, $locale) = @_;
 
   (
 
   (
-    $self->part_svc->svc,
+    $self->part_svc->svc_locale($locale),
     $svc_x->$method(@_),
     $self->part_svc->svcdb,
     $self->svcnum
     $svc_x->$method(@_),
     $self->part_svc->svcdb,
     $self->svcnum
@@ -611,13 +831,12 @@ sub seconds_since { 'internal session db deprecated'; };
 
 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
 
 
 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
 
-See L<FS::svc_acct/seconds_since_sqlradacct>.  Equivalent to
-$cust_svc->svc_x->seconds_since_sqlradacct, but more efficient.  Meaningless
-for records where B<svcdb> is not "svc_acct".
+Equivalent to $cust_svc->svc_x->seconds_since_sqlradacct, but 
+more efficient.  Meaningless for records where B<svcdb> is not 
+svc_acct or svc_broadband.
 
 =cut
 
 
 =cut
 
-#note: implementation here, POD in FS::svc_acct
 sub seconds_since_sqlradacct {
   my($self, $start, $end) = @_;
 
 sub seconds_since_sqlradacct {
   my($self, $start, $end) = @_;
 
@@ -639,9 +858,9 @@ sub seconds_since_sqlradacct {
     warn "$mes connecting to sqlradius database\n"
       if $DEBUG;
 
     warn "$mes connecting to sqlradius database\n"
       if $DEBUG;
 
-    my $dbh = DBI->connect( map { $part_export->option($_) }
+    my $dbh = FS::DBI->connect( map { $part_export->option($_) }
                             qw(datasrc username password)    )
                             qw(datasrc username password)    )
-      or die "can't connect to sqlradius database: ". $DBI::errstr;
+      or die "can't connect to sqlradius database: ". $FS::DBI::errstr;
 
     warn "$mes connected to sqlradius database\n"
       if $DEBUG;
 
     warn "$mes connected to sqlradius database\n"
       if $DEBUG;
@@ -756,12 +975,11 @@ sub seconds_since_sqlradacct {
 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
 
 See L<FS::svc_acct/attribute_since_sqlradacct>.  Equivalent to
 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
 
 See L<FS::svc_acct/attribute_since_sqlradacct>.  Equivalent to
-$cust_svc->svc_x->attribute_since_sqlradacct, but more efficient.  Meaningless
-for records where B<svcdb> is not "svc_acct".
+$cust_svc->svc_x->attribute_since_sqlradacct, but more efficient.
+Meaningless for records where B<svcdb> is not svc_acct or svc_broadband.
 
 =cut
 
 
 =cut
 
-#note: implementation here, POD in FS::svc_acct
 #(false laziness w/seconds_since_sqlradacct above)
 sub attribute_since_sqlradacct {
   my($self, $start, $end, $attrib) = @_;
 #(false laziness w/seconds_since_sqlradacct above)
 sub attribute_since_sqlradacct {
   my($self, $start, $end, $attrib) = @_;
@@ -785,9 +1003,9 @@ sub attribute_since_sqlradacct {
     warn "$mes connecting to sqlradius database\n"
       if $DEBUG;
 
     warn "$mes connecting to sqlradius database\n"
       if $DEBUG;
 
-    my $dbh = DBI->connect( map { $part_export->option($_) }
+    my $dbh = FS::DBI->connect( map { $part_export->option($_) }
                             qw(datasrc username password)    )
                             qw(datasrc username password)    )
-      or die "can't connect to sqlradius database: ". $DBI::errstr;
+      or die "can't connect to sqlradius database: ". $FS::DBI::errstr;
 
     warn "$mes connected to sqlradius database\n"
       if $DEBUG;
 
     warn "$mes connected to sqlradius database\n"
       if $DEBUG;
@@ -856,9 +1074,9 @@ sub attribute_last_sqlradacct {
     warn "$mes connecting to sqlradius database\n"
       if $DEBUG;
 
     warn "$mes connecting to sqlradius database\n"
       if $DEBUG;
 
-    my $dbh = DBI->connect( map { $part_export->option($_) }
+    my $dbh = FS::DBI->connect( map { $part_export->option($_) }
                             qw(datasrc username password)    )
                             qw(datasrc username password)    )
-      or die "can't connect to sqlradius database: ". $DBI::errstr;
+      or die "can't connect to sqlradius database: ". $FS::DBI::errstr;
 
     warn "$mes connected to sqlradius database\n"
       if $DEBUG;
 
     warn "$mes connected to sqlradius database\n"
       if $DEBUG;
@@ -982,7 +1200,7 @@ sub API_getinfo {
   my $self = shift;
   my $svc_x = $self->svc_x;
  +{ ( map { $_=>$self->$_ } $self->fields ),
   my $self = shift;
   my $svc_x = $self->svc_x;
  +{ ( map { $_=>$self->$_ } $self->fields ),
-    ( map { $svc_x=>$svc_x->$_ } $svc_x->fields ),
+    ( map { $_=>$svc_x->$_ } $svc_x->fields ),
   };
 }
 
   };
 }
 
@@ -1018,9 +1236,10 @@ sub smart_search_param {
   my @or = 
       map { my $table = $_;
             my $search_sql = "FS::$table"->search_sql($string);
   my @or = 
       map { my $table = $_;
             my $search_sql = "FS::$table"->search_sql($string);
+            my $addl_from = "FS::$table"->search_sql_addl_from();
 
             "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
 
             "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
-            "FROM $table WHERE $search_sql";
+            "FROM $table $addl_from WHERE $search_sql";
           }
       FS::part_svc->svc_tables;
 
           }
       FS::part_svc->svc_tables;
 
@@ -1053,6 +1272,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 _check_provision_hold {
+  my $self = shift;
+
+  # check status of cust_pkg
+  my $cust_pkg = $self->cust_pkg;
+  return '' unless $cust_pkg && $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;
 
@@ -1083,13 +1331,19 @@ sub _upgrade_data {
     my $svcdb = $cust_svc->part_svc->svcdb;
     $h_search{'hashref'}{'svcnum'} = $svcnum;
     $h_search{'table'} = "h_$svcdb";
     my $svcdb = $cust_svc->part_svc->svcdb;
     $h_search{'hashref'}{'svcnum'} = $svcnum;
     $h_search{'table'} = "h_$svcdb";
-    my $h_svc_x = qsearchs(\%h_search)
-      or next;
-    my $class = "FS::$svcdb";
-    my $new_svc_x = $class->new({ $h_svc_x->hash });
-    my $error = $new_svc_x->insert;
-    warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
-      if $error;
+    my $h_svc_x = qsearchs(\%h_search);
+    if ( $h_svc_x ) {
+      my $class = "FS::$svcdb";
+      my $new_svc_x = $class->new({ $h_svc_x->hash });
+      my $error = $new_svc_x->insert;
+      warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
+        if $error;
+    } else {
+      # can't be fixed, so remove the dangling cust_svc to avoid breaking
+      # stuff
+      my $error = $cust_svc->delete;
+      warn "error cleaning up missing svcnum $svcnum ($svcdb):\n$error\n";
+    }
   }
 
   '';
   }
 
   '';