show credit balance on invoices, #11564
[freeside.git] / FS / FS / svc_Common.pm
index 52226d2..5c6e16b 100644 (file)
@@ -1,8 +1,10 @@
 package FS::svc_Common;
 
 use strict;
 package FS::svc_Common;
 
 use strict;
-use vars qw( @ISA $noexport_hack $DEBUG $me );
-use Carp qw( cluck carp croak ); #specify cluck have to specify them all..
+use vars qw( @ISA $noexport_hack $DEBUG $me
+             $overlimit_missing_cust_svc_nonfatal_kludge );
+use Carp qw( cluck carp croak confess ); #specify cluck have to specify them all
+use Scalar::Util qw( blessed );
 use FS::Record qw( qsearch qsearchs fields dbh );
 use FS::cust_main_Mixin;
 use FS::cust_svc;
 use FS::Record qw( qsearch qsearchs fields dbh );
 use FS::cust_main_Mixin;
 use FS::cust_svc;
@@ -17,6 +19,8 @@ use FS::inventory_class;
 $me = '[FS::svc_Common]';
 $DEBUG = 0;
 
 $me = '[FS::svc_Common]';
 $DEBUG = 0;
 
+$overlimit_missing_cust_svc_nonfatal_kludge = 0;
+
 =head1 NAME
 
 FS::svc_Common - Object method for all svc_ records
 =head1 NAME
 
 FS::svc_Common - Object method for all svc_ records
@@ -40,13 +44,15 @@ inherit from, i.e. FS::svc_acct.  FS::svc_Common inherits from FS::Record.
 
 Class method which returns an SQL fragment to search for STRING in FIELD.
 
 
 Class method which returns an SQL fragment to search for STRING in FIELD.
 
+It is now case-insensitive by default.
+
 =cut
 
 sub search_sql_field {
   my( $class, $field, $string ) = @_;
   my $table = $class->table;
   my $q_string = dbh->quote($string);
 =cut
 
 sub search_sql_field {
   my( $class, $field, $string ) = @_;
   my $table = $class->table;
   my $q_string = dbh->quote($string);
-  "$table.$field = $q_string";
+  "LOWER($table.$field) = LOWER($q_string)";
 }
 
 #fallback for services that don't provide a search... 
 }
 
 #fallback for services that don't provide a search... 
@@ -128,7 +134,7 @@ sub virtual_fields {
     my %flags = map { $_->columnname, $_->columnflag } (
         qsearch ('part_svc_column', { svcpart => $svcpart } )
       );
     my %flags = map { $_->columnname, $_->columnflag } (
         qsearch ('part_svc_column', { svcpart => $svcpart } )
       );
-    return grep { not ($flags{$_} eq 'X') } @vfields;
+    return grep { not ( defined($flags{$_}) && $flags{$_} eq 'X') } @vfields;
   } else { # Case 3
     return @vfields;
   } 
   } else { # Case 3
     return @vfields;
   } 
@@ -148,6 +154,11 @@ sub label {
   $self->svcnum;
 }
 
   $self->svcnum;
 }
 
+sub label_long {
+  my $self = shift;
+  $self->label(@_);
+}
+
 =item check
 
 Checks the validity of fields in this record.
 =item check
 
 Checks the validity of fields in this record.
@@ -187,6 +198,9 @@ If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
 jobnums), all provisioning jobs will have a dependancy on the supplied
 jobnum(s) (they will not run until the specific job(s) complete(s)).
 
 jobnums), all provisioning jobs will have a dependancy on the supplied
 jobnum(s) (they will not run until the specific job(s) complete(s)).
 
+If I<export_args> is set to an array reference, the referenced list will be
+passed to export commands.
+
 =cut
 
 sub insert {
 =cut
 
 sub insert {
@@ -203,7 +217,6 @@ sub insert {
   my $objects = $options{'child_objects'} || [];
   my $depend_jobnums = $options{'depend_jobnum'} || [];
   $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
   my $objects = $options{'child_objects'} || [];
   my $depend_jobnums = $options{'depend_jobnum'} || [];
   $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
-  my $error;
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
@@ -216,9 +229,6 @@ sub insert {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  $error = $self->check;
-  return $error if $error;
-
   my $svcnum = $self->svcnum;
   my $cust_svc = $svcnum ? qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) : '';
   #unless ( $svcnum ) {
   my $svcnum = $self->svcnum;
   my $cust_svc = $svcnum ? qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) : '';
   #unless ( $svcnum ) {
@@ -229,7 +239,7 @@ sub insert {
       'pkgnum'  => $self->pkgnum,
       'svcpart' => $self->svcpart,
     } );
       'pkgnum'  => $self->pkgnum,
       'svcpart' => $self->svcpart,
     } );
-    $error = $cust_svc->insert;
+    my $error = $cust_svc->insert;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return $error;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return $error;
@@ -245,13 +255,12 @@ sub insert {
     $self->svcpart($cust_svc->svcpart);
   }
 
     $self->svcpart($cust_svc->svcpart);
   }
 
-  $error = $self->set_auto_inventory;
-  if ( $error ) {
-    $dbh->rollback if $oldAutoCommit;
-    return $error;
-  }
-
-  $error = $self->SUPER::insert;
+  my $error =    $self->preinsert_hook_first
+              || $self->set_auto_inventory
+              || $self->check
+              || $self->_check_duplicate
+              || $self->preinsert_hook
+              || $self->SUPER::insert;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -279,8 +288,10 @@ sub insert {
     warn "[$me] insert: \$FS::queue::jobnums is $FS::queue::jobnums\n"
       if $DEBUG;
 
     warn "[$me] insert: \$FS::queue::jobnums is $FS::queue::jobnums\n"
       if $DEBUG;
 
+    my $export_args = $options{'export_args'} || [];
+
     foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
     foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
-      my $error = $part_export->export_insert($self);
+      my $error = $part_export->export_insert($self, @$export_args);
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
         return "exporting to ". $part_export->exporttype.
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
         return "exporting to ". $part_export->exporttype.
@@ -314,7 +325,15 @@ sub insert {
   '';
 }
 
   '';
 }
 
-=item delete
+#fallbacks
+sub preinsert_hook_first { ''; }
+sub _check_duplcate { ''; }
+sub preinsert_hook { ''; }
+sub table_dupcheck_fields { (); }
+sub predelete_hook { ''; }
+sub predelete_hook_first { ''; }
+
+=item delete [ , OPTION => VALUE ... ]
 
 Deletes this account from the database.  If there is an error, returns the
 error, otherwise returns false.
 
 Deletes this account from the database.  If there is an error, returns the
 error, otherwise returns false.
@@ -325,7 +344,8 @@ The corresponding FS::cust_svc record will be deleted as well.
 
 sub delete {
   my $self = shift;
 
 sub delete {
   my $self = shift;
-  my $error;
+  my %options = @_;
+  my $export_args = $options{'export_args'} || [];
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
@@ -338,10 +358,12 @@ sub delete {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  $error =    $self->SUPER::delete
-           || $self->export('delete')
-          || $self->return_inventory
-          || $self->cust_svc->delete
+  my $error =  $self->predelete_hook_first 
+             || $self->SUPER::delete
+              || $self->export('delete', @$export_args)
+             || $self->return_inventory
+             || $self->predelete_hook
+             || $self->cust_svc->delete
   ;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
   ;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
@@ -353,15 +375,74 @@ sub delete {
   '';
 }
 
   '';
 }
 
-=item replace OLD_RECORD
+=item expire DATE
+
+Currently this will only run expire exports if any are attached
+
+=cut
+
+sub expire {
+  my($self,$date) = (shift,shift);
+
+  return 'Expire date must be specified' unless $date;
+    
+  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 $export_args = [$date];
+  my $error = $self->export('expire', @$export_args);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+}
+
+=item replace [ OLD_RECORD ] [ HASHREF | OPTION => VALUE ]
 
 Replaces OLD_RECORD with this one.  If there is an error, returns the error,
 otherwise returns false.
 
 
 Replaces OLD_RECORD with this one.  If there is an error, returns the error,
 otherwise returns false.
 
+Currently available options are: I<export_args> and I<depend_jobnum>.
+
+If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
+jobnums), all provisioning jobs will have a dependancy on the supplied
+jobnum(s) (they will not run until the specific job(s) complete(s)).
+
+If I<export_args> is set to an array reference, the referenced list will be
+passed to export commands.
+
 =cut
 
 sub replace {
 =cut
 
 sub replace {
-  my ($new, $old) = (shift, shift);
+  my $new = shift;
+
+  my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
+              ? shift
+              : $new->replace_old;
+
+  my $options = 
+    ( ref($_[0]) eq 'HASH' )
+      ? shift
+      : { @_ };
+
+  my @jobnums = ();
+  local $FS::queue::jobnums = \@jobnums;
+  warn "[$me] replace: set \$FS::queue::jobnums to $FS::queue::jobnums\n"
+    if $DEBUG;
+  my $depend_jobnums = $options->{'depend_jobnum'} || [];
+  $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
@@ -374,15 +455,31 @@ sub replace {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  # We absolutely have to have an old vs. new record to make this work.
-  $old = $new->replace_old unless defined($old);
+  my $error = $new->set_auto_inventory($old);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
 
 
-  my $error = $new->set_auto_inventory;
+  #redundant, but so any duplicate fields are maniuplated as appropriate
+  # (svc_phone.phonenum)
+  $error = $new->check;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   }
 
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   }
 
+  #if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
+  if ( grep { $old->$_ ne $new->$_ } $new->table_dupcheck_fields ) {
+
+    $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
+    $error = $new->_check_duplicate;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
   $error = $new->SUPER::replace($old);
   if ($error) {
     $dbh->rollback if $oldAutoCommit;
   $error = $new->SUPER::replace($old);
   if ($error) {
     $dbh->rollback if $oldAutoCommit;
@@ -392,6 +489,11 @@ sub replace {
   #new-style exports!
   unless ( $noexport_hack ) {
 
   #new-style exports!
   unless ( $noexport_hack ) {
 
+    warn "[$me] replace: \$FS::queue::jobnums is $FS::queue::jobnums\n"
+      if $DEBUG;
+
+    my $export_args = $options->{'export_args'} || [];
+
     #not quite false laziness, but same pattern as FS::svc_acct::replace and
     #FS::part_export::sqlradius::_export_replace.  List::Compare or something
     #would be useful but too much of a pain in the ass to deploy
     #not quite false laziness, but same pattern as FS::svc_acct::replace and
     #FS::part_export::sqlradius::_export_replace.  List::Compare or something
     #would be useful but too much of a pain in the ass to deploy
@@ -407,7 +509,7 @@ sub replace {
     foreach my $delete_part_export (
       grep { ! $new_exportnum{$_->exportnum} } @old_part_export
     ) {
     foreach my $delete_part_export (
       grep { ! $new_exportnum{$_->exportnum} } @old_part_export
     ) {
-      my $error = $delete_part_export->export_delete($old);
+      my $error = $delete_part_export->export_delete($old, @$export_args);
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
         return "error deleting, export to ". $delete_part_export->exporttype.
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
         return "error deleting, export to ". $delete_part_export->exporttype.
@@ -418,7 +520,8 @@ sub replace {
     foreach my $replace_part_export (
       grep { $old_exportnum{$_->exportnum} } @new_part_export
     ) {
     foreach my $replace_part_export (
       grep { $old_exportnum{$_->exportnum} } @new_part_export
     ) {
-      my $error = $replace_part_export->export_replace($new,$old);
+      my $error =
+        $replace_part_export->export_replace( $new, $old, @$export_args);
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
         return "error exporting to ". $replace_part_export->exporttype.
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
         return "error exporting to ". $replace_part_export->exporttype.
@@ -429,7 +532,7 @@ sub replace {
     foreach my $insert_part_export (
       grep { ! $old_exportnum{$_->exportnum} } @new_part_export
     ) {
     foreach my $insert_part_export (
       grep { ! $old_exportnum{$_->exportnum} } @new_part_export
     ) {
-      my $error = $insert_part_export->export_insert($new);
+      my $error = $insert_part_export->export_insert($new, @$export_args );
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
         return "error inserting export to ". $insert_part_export->exporttype.
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
         return "error inserting export to ". $insert_part_export->exporttype.
@@ -437,13 +540,27 @@ sub replace {
       }
     }
 
       }
     }
 
+    foreach my $depend_jobnum ( @$depend_jobnums ) {
+      warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
+        if $DEBUG;
+      foreach my $jobnum ( @jobnums ) {
+        my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
+        warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
+          if $DEBUG;
+        my $error = $queue->depend_insert($depend_jobnum);
+        if ( $error ) {
+          $dbh->rollback if $oldAutoCommit;
+          return "error queuing job dependancy: $error";
+        }
+      }
+    }
+
   }
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   '';
 }
 
   }
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   '';
 }
 
-
 =item setfixed
 
 Sets any fixed fields for this service (see L<FS::part_svc>).  If there is an
 =item setfixed
 
 Sets any fixed fields for this service (see L<FS::part_svc>).  If there is an
@@ -502,7 +619,7 @@ sub setx {
   return $error if $error;
 
   my $part_svc = $self->part_svc;
   return $error if $error;
 
   my $part_svc = $self->part_svc;
-  return "Unkonwn svcpart" unless $part_svc;
+  return "Unknown svcpart" unless $part_svc;
 
   #set default/fixed/whatever fields from part_svc
 
 
   #set default/fixed/whatever fields from part_svc
 
@@ -541,15 +658,115 @@ sub part_svc {
 
 }
 
 
 }
 
+=item svc_pbx
+
+Returns the FS::svc_pbx record for this service, if any (see L<FS::svc_pbx>).
+
+Only makes sense if the service has a pbxsvc field (currently, svc_phone and
+svc_acct).
+
+=cut
+
+# XXX FS::h_svc_{acct,phone} could have a history-aware svc_pbx override
+
+sub svc_pbx {
+  my $self = shift;
+  return '' unless $self->pbxsvc;
+  qsearchs( 'svc_pbx', { 'svcnum' => $self->pbxsvc } );
+}
+
+=item pbx_title
+
+Returns the title of the FS::svc_pbx record associated with this service, if
+any.
+
+Only makes sense if the service has a pbxsvc field (currently, svc_phone and
+svc_acct).
+
+=cut
+
+sub pbx_title {
+  my $self = shift;
+  my $svc_pbx = $self->svc_pbx or return '';
+  $svc_pbx->title;
+}
+
+=item pbx_select_hash %OPTIONS
+
+Can be called as an object method or a class method.
+
+Returns a hash SVCNUM => TITLE ...  representing the PBXes this customer
+that may be associated with this service.
+
+Currently available options are: I<pkgnum> I<svcpart>
+
+Only makes sense if the service has a pbxsvc field (currently, svc_phone and
+svc_acct).
+
+=cut
+
+#false laziness w/svc_acct::domain_select_hash
+sub pbx_select_hash {
+  my ($self, %options) = @_;
+  my %pbxes = ();
+  my $part_svc;
+  my $cust_pkg;
+
+  if (ref($self)) {
+    $part_svc = $self->part_svc;
+    $cust_pkg = $self->cust_svc->cust_pkg
+      if $self->cust_svc;
+  }
+
+  $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
+    if $options{'svcpart'};
+
+  $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
+    if $options{'pkgnum'};
+
+  if ($part_svc && ( $part_svc->part_svc_column('pbxsvc')->columnflag eq 'S'
+                  || $part_svc->part_svc_column('pbxsvc')->columnflag eq 'F')) {
+    %pbxes = map { $_->svcnum => $_->title }
+             map { qsearchs('svc_pbx', { 'svcnum' => $_ }) }
+             split(',', $part_svc->part_svc_column('pbxsvc')->columnvalue);
+  } elsif ($cust_pkg) { # && !$conf->exists('svc_acct-alldomains') ) {
+    %pbxes = map { $_->svcnum => $_->title }
+             map { qsearchs('svc_pbx', { 'svcnum' => $_->svcnum }) }
+             map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
+             qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
+  } else {
+    #XXX agent-virt
+    %pbxes = map { $_->svcnum => $_->title } qsearch('svc_pbx', {} );
+  }
+
+  if ($part_svc && $part_svc->part_svc_column('pbxsvc')->columnflag eq 'D') {
+    my $svc_pbx = qsearchs('svc_pbx',
+      { 'svcnum' => $part_svc->part_svc_column('pbxsvc')->columnvalue } );
+    if ( $svc_pbx ) {
+      $pbxes{$svc_pbx->svcnum}  = $svc_pbx->title;
+    } else {
+      warn "unknown svc_pbx.svcnum for part_svc_column pbxsvc: ".
+           $part_svc->part_svc_column('pbxsvc')->columnvalue;
+
+    }
+  }
+
+  (%pbxes);
+
+}
+
 =item set_auto_inventory
 
 =item set_auto_inventory
 
-Sets any fields which auto-populate from inventory (see L<FS::part_svc>).
+Sets any fields which auto-populate from inventory (see L<FS::part_svc>), and
+also check any manually populated inventory fields.
+
 If there is an error, returns the error, otherwise returns false.
 
 =cut
 
 sub set_auto_inventory {
   my $self = shift;
 If there is an error, returns the error, otherwise returns false.
 
 =cut
 
 sub set_auto_inventory {
   my $self = shift;
+  my $old = @_ ? shift : '';
 
   my $error =
     $self->ut_numbern('svcnum')
 
   my $error =
     $self->ut_numbern('svcnum')
@@ -573,39 +790,76 @@ sub set_auto_inventory {
   #set default/fixed/whatever fields from part_svc
   my $table = $self->table;
   foreach my $field ( grep { $_ ne 'svcnum' } $self->fields ) {
   #set default/fixed/whatever fields from part_svc
   my $table = $self->table;
   foreach my $field ( grep { $_ ne 'svcnum' } $self->fields ) {
+
     my $part_svc_column = $part_svc->part_svc_column($field);
     my $part_svc_column = $part_svc->part_svc_column($field);
-    if ( $part_svc_column->columnflag eq 'A' && $self->$field() eq '' ) {
-
-      my $classnum = $part_svc_column->columnvalue;
-      my $inventory_item = qsearchs({
-        'table'     => 'inventory_item',
-        'hashref'   => { 'classnum' => $classnum, 
-                         'svcnum'   => '',
-                       },
-        'extra_sql' => 'LIMIT 1 FOR UPDATE',
-      });
+    my $columnflag = $part_svc_column->columnflag;
+    next unless $columnflag =~ /^[AM]$/;
 
 
-      unless ( $inventory_item ) {
-        $dbh->rollback if $oldAutoCommit;
-        my $inventory_class =
-          qsearchs('inventory_class', { 'classnum' => $classnum } );
-        return "Can't find inventory_class.classnum $classnum"
-          unless $inventory_class;
-        return "Out of ". $inventory_class->classname. "s\n"; #Lingua:: BS
-                                                              #for pluralizing
-      }
+    next if $columnflag eq 'A' && $self->$field() ne '';
 
 
-      $inventory_item->svcnum( $self->svcnum );
-      my $ierror = $inventory_item->replace();
-      if ( $ierror ) {
-        $dbh->rollback if $oldAutoCommit;
-        return "Error provisioning inventory: $ierror";
-        
-      }
+    my $classnum = $part_svc_column->columnvalue;
+    my %hash = ( 'classnum' => $classnum );
 
 
-      $self->setfield( $field, $inventory_item->item );
+    if ( $columnflag eq 'A' && $self->$field() eq '' ) {
+      $hash{'svcnum'} = '';
+    } elsif ( $columnflag eq 'M' ) {
+      return "Select inventory item for $field" unless $self->getfield($field);
+      $hash{'item'} = $self->getfield($field);
+    }
+
+    my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql(
+      'null'  => 1,
+      'table' => 'inventory_item',
+    );
+
+    my $inventory_item = qsearchs({
+      'table'     => 'inventory_item',
+      'hashref'   => \%hash,
+      'extra_sql' => "AND $agentnums_sql",
+      'order_by'  => 'ORDER BY ( agentnum IS NULL ) '. #agent inventory first
+                     ' LIMIT 1 FOR UPDATE',
+    });
 
 
+    unless ( $inventory_item ) {
+      $dbh->rollback if $oldAutoCommit;
+      my $inventory_class =
+        qsearchs('inventory_class', { 'classnum' => $classnum } );
+      return "Can't find inventory_class.classnum $classnum"
+        unless $inventory_class;
+      return "Out of ". $inventory_class->classname. "s\n"; #Lingua:: BS
+                                                            #for pluralizing
     }
     }
+
+    next if $columnflag eq 'M' && $inventory_item->svcnum == $self->svcnum;
+
+    $self->setfield( $field, $inventory_item->item );
+      #if $columnflag eq 'A' && $self->$field() eq '';
+
+    $inventory_item->svcnum( $self->svcnum );
+    my $ierror = $inventory_item->replace();
+    if ( $ierror ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Error provisioning inventory: $ierror";
+    }
+
+    if ( $old && $old->$field() && $old->$field() ne $self->$field() ) {
+      my $old_inv = qsearchs({
+        'table'   => 'inventory_item',
+        'hashref' => { 'classnum' => $classnum,
+                       'svcnum'   => $old->svcnum,
+                       'item'     => $old->$field(),
+                     },
+      });
+      if ( $old_inv ) {
+        $old_inv->svcnum('');
+        my $oerror = $old_inv->replace;
+        if ( $oerror ) {
+          $dbh->rollback if $oldAutoCommit;
+          return "Error unprovisioning inventory: $oerror";
+        }
+      }
+    }
+
   }
 
  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   }
 
  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
@@ -681,7 +935,9 @@ Runs export_suspend callbacks.
 
 sub suspend {
   my $self = shift;
 
 sub suspend {
   my $self = shift;
-  $self->export('suspend');
+  my %options = @_;
+  my $export_args = $options{'export_args'} || [];
+  $self->export('suspend', @$export_args);
 }
 
 =item unsuspend
 }
 
 =item unsuspend
@@ -692,7 +948,41 @@ Runs export_unsuspend callbacks.
 
 sub unsuspend {
   my $self = shift;
 
 sub unsuspend {
   my $self = shift;
-  $self->export('unsuspend');
+  my %options = @_;
+  my $export_args = $options{'export_args'} || [];
+  $self->export('unsuspend', @$export_args);
+}
+
+=item export_links
+
+Runs export_links callbacks and returns the links.
+
+=cut
+
+sub export_links {
+  my $self = shift;
+  my $return = [];
+  $self->export('links', $return);
+  $return;
+}
+
+=item export_getsettings
+
+Runs export_getsettings callbacks and returns the two hashrefs.
+
+=cut
+
+sub export_getsettings {
+  my $self = shift;
+  my %settings = ();
+  my %defaults = ();
+  my $error = $self->export('getsettings', \%settings, \%defaults);
+  if ( $error ) {
+    #XXX bubble this up better
+    warn "error running export_getsetings: $error";
+    return ( {}, {} );
+  }
+  ( \%settings, \%defaults );
 }
 
 =item export HOOK [ EXPORT_ARGS ]
 }
 
 =item export HOOK [ EXPORT_ARGS ]
@@ -743,7 +1033,19 @@ Sets or retrieves overlimit date.
 
 sub overlimit {
   my $self = shift;
 
 sub overlimit {
   my $self = shift;
-  $self->cust_svc->overlimit(@_);
+  #$self->cust_svc->overlimit(@_);
+  my $cust_svc = $self->cust_svc;
+  unless ( $cust_svc ) { #wtf?
+    my $error = "$me overlimit: missing cust_svc record for svc_acct svcnum ".
+                $self->svcnum;
+    if ( $overlimit_missing_cust_svc_nonfatal_kludge ) {
+      cluck "$error; continuing anyway as requested";
+      return '';
+    } else {
+      confess $error;
+    }
+  }
+  $cust_svc->overlimit(@_);
 }
 
 =item cancel
 }
 
 =item cancel
@@ -783,6 +1085,42 @@ sub clone_kludge_unsuspend {
   shift;
 }
 
   shift;
 }
 
+=item find_duplicates MODE FIELDS...
+
+Method used by _check_duplicate routines to find services with duplicate 
+values in specified fields.  Set MODE to 'global' to search across all 
+services, or 'export' to limit to those that share one or more exports 
+with this service.  FIELDS is a list of field names; only services 
+matching in all fields will be returned.  Empty fields will be skipped.
+
+=cut
+
+sub find_duplicates {
+  my $self = shift;
+  my $mode = shift;
+  my @fields = @_;
+
+  my %search = map { $_ => $self->getfield($_) } 
+               grep { length($self->getfield($_)) } @fields;
+  return () if !%search;
+  my @dup = grep { ! $self->svcnum or $_->svcnum != $self->svcnum }
+            qsearch( $self->table, \%search );
+  return () if !@dup;
+  return @dup if $mode eq 'global';
+  die "incorrect find_duplicates mode '$mode'" if $mode ne 'export';
+
+  my $exports = FS::part_export::export_info($self->table);
+  my %conflict_svcparts;
+  my $part_svc = $self->part_svc;
+  foreach my $part_export ( $part_svc->part_export ) {
+    %conflict_svcparts = map { $_->svcpart => 1 } $part_export->export_svc;
+  }
+  return grep { $conflict_svcparts{$_->cust_svc->svcpart} } @dup;
+}
+
+
+
+
 =back
 
 =head1 BUGS
 =back
 
 =head1 BUGS