1 package FS::svc_Common;
4 use vars qw( @ISA $noexport_hack $DEBUG $me
5 $overlimit_missing_cust_svc_nonfatal_kludge );
6 use Carp qw( cluck carp croak confess ); #specify cluck have to specify them all
7 use Scalar::Util qw( blessed );
8 use Lingua::EN::Inflect qw( PL_N );
10 use FS::Record qw( qsearch qsearchs fields dbh );
11 use FS::cust_main_Mixin;
16 use FS::inventory_item;
17 use FS::inventory_class;
18 use FS::NetworkMonitoringSystem;
20 @ISA = qw( FS::cust_main_Mixin FS::Record );
22 $me = '[FS::svc_Common]';
25 $overlimit_missing_cust_svc_nonfatal_kludge = 0;
29 FS::svc_Common - Object method for all svc_ records
35 @ISA = qw( FS::svc_Common );
39 FS::svc_Common is intended as a base class for table-specific classes to
40 inherit from, i.e. FS::svc_acct. FS::svc_Common inherits from FS::Record.
46 =item search_sql_field FIELD STRING
48 Class method which returns an SQL fragment to search for STRING in FIELD.
50 It is now case-insensitive by default.
54 sub search_sql_field {
55 my( $class, $field, $string ) = @_;
56 my $table = $class->table;
57 my $q_string = dbh->quote($string);
58 "LOWER($table.$field) = LOWER($q_string)";
61 #fallback for services that don't provide a search...
63 #my( $class, $string ) = @_;
73 my $class = ref($proto) || $proto;
75 bless ($self, $class);
77 unless ( defined ( $self->table ) ) {
78 $self->{'Table'} = shift;
79 carp "warning: FS::Record::new called with table name ". $self->{'Table'};
82 #$self->{'Hash'} = shift;
84 $self->{'Hash'} = { map { $_ => $newhash->{$_} } qw(svcnum svcpart) };
86 $self->setdefault( $self->_fieldhandlers )
89 $self->{'Hash'}{$_} = $newhash->{$_}
90 foreach grep { defined($newhash->{$_}) && length($newhash->{$_}) }
93 foreach my $field ( grep !defined($self->{'Hash'}{$_}), $self->fields ) {
94 $self->{'Hash'}{$field}='';
97 $self->_rebless if $self->can('_rebless');
99 $self->{'modified'} = 0;
101 $self->_cache($self->{'Hash'}, shift) if $self->can('_cache') && @_;
107 sub _fieldhandlers { {}; }
111 # This restricts the fields based on part_svc_column and the svcpart of
112 # the service. There are four possible cases:
113 # 1. svcpart passed as part of the svc_x hash.
114 # 2. svcpart fetched via cust_svc based on svcnum.
115 # 3. No svcnum or svcpart. In this case, return ALL the fields with
116 # dbtable eq $self->table.
117 # 4. Called via "fields('svc_acct')" or something similar. In this case
118 # there is no $self object.
122 my @vfields = $self->SUPER::virtual_fields;
124 return @vfields unless (ref $self); # Case 4
126 if ($self->svcpart) { # Case 1
127 $svcpart = $self->svcpart;
128 } elsif ( $self->svcnum
129 && qsearchs('cust_svc',{'svcnum'=>$self->svcnum} )
131 $svcpart = $self->cust_svc->svcpart;
136 if ($svcpart) { #Cases 1 and 2
137 my %flags = map { $_->columnname, $_->columnflag } (
138 qsearch ('part_svc_column', { svcpart => $svcpart } )
140 return grep { not ( defined($flags{$_}) && $flags{$_} eq 'X') } @vfields;
149 svc_Common provides a fallback label subroutine that just returns the svcnum.
155 cluck "warning: ". ref($self). " not loaded or missing label method; ".
167 (($self->cust_svc || return)->cust_pkg || return)->cust_main || return
172 defined($self->cust_main);
177 Checks the validity of fields in this record.
179 At present, this does nothing but call FS::Record::check (which, in turn,
180 does nothing but run virtual field checks).
189 =item insert [ , OPTION => VALUE ... ]
191 Adds this record to the database. If there is an error, returns the error,
192 otherwise returns false.
194 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
195 defined. An FS::cust_svc record will be created and inserted.
197 Currently available options are: I<jobnums>, I<child_objects> and
200 If I<jobnum> is set to an array reference, the jobnums of any export jobs will
201 be added to the referenced array.
203 If I<child_objects> is set to an array reference of FS::tablename objects (for
204 example, FS::acct_snarf objects), they will have their svcnum field set and
205 will be inserted after this record, but before any exports are run. Each
206 element of the array can also optionally be a two-element array reference
207 containing the child object and the name of an alternate field to be filled in
208 with the newly-inserted svcnum, for example C<[ $svc_forward, 'srcsvc' ]>
210 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
211 jobnums), all provisioning jobs will have a dependancy on the supplied
212 jobnum(s) (they will not run until the specific job(s) complete(s)).
214 If I<export_args> is set to an array reference, the referenced list will be
215 passed to export commands.
222 warn "[$me] insert called with options ".
223 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
227 local $FS::queue::jobnums = \@jobnums;
228 warn "[$me] insert: set \$FS::queue::jobnums to $FS::queue::jobnums\n"
230 my $objects = $options{'child_objects'} || [];
231 my $depend_jobnums = $options{'depend_jobnum'} || [];
232 $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
234 local $SIG{HUP} = 'IGNORE';
235 local $SIG{INT} = 'IGNORE';
236 local $SIG{QUIT} = 'IGNORE';
237 local $SIG{TERM} = 'IGNORE';
238 local $SIG{TSTP} = 'IGNORE';
239 local $SIG{PIPE} = 'IGNORE';
241 my $oldAutoCommit = $FS::UID::AutoCommit;
242 local $FS::UID::AutoCommit = 0;
245 my $svcnum = $self->svcnum;
246 my $cust_svc = $svcnum ? qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) : '';
247 #unless ( $svcnum ) {
248 if ( !$svcnum or !$cust_svc ) {
249 $cust_svc = new FS::cust_svc ( {
250 #hua?# 'svcnum' => $svcnum,
251 'svcnum' => $self->svcnum,
252 'pkgnum' => $self->pkgnum,
253 'svcpart' => $self->svcpart,
255 my $error = $cust_svc->insert;
257 $dbh->rollback if $oldAutoCommit;
260 $svcnum = $self->svcnum($cust_svc->svcnum);
262 #$cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
263 unless ( $cust_svc ) {
264 $dbh->rollback if $oldAutoCommit;
265 return "no cust_svc record found for svcnum ". $self->svcnum;
267 $self->pkgnum($cust_svc->pkgnum);
268 $self->svcpart($cust_svc->svcpart);
271 my $error = $self->preinsert_hook_first
272 || $self->set_auto_inventory
274 || $self->_check_duplicate
275 || $self->preinsert_hook
276 || $self->SUPER::insert;
278 $dbh->rollback if $oldAutoCommit;
282 foreach my $object ( @$objects ) {
284 if ( ref($object) eq 'ARRAY' ) {
285 ($obj, $field) = @$object;
290 $obj->$field($self->svcnum);
291 $error = $obj->insert;
293 $dbh->rollback if $oldAutoCommit;
299 unless ( $noexport_hack ) {
301 warn "[$me] insert: \$FS::queue::jobnums is $FS::queue::jobnums\n"
304 my $export_args = $options{'export_args'} || [];
306 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
307 my $error = $part_export->export_insert($self, @$export_args);
309 $dbh->rollback if $oldAutoCommit;
310 return "exporting to ". $part_export->exporttype.
311 " (transaction rolled back): $error";
315 foreach my $depend_jobnum ( @$depend_jobnums ) {
316 warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
318 foreach my $jobnum ( @jobnums ) {
319 my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
320 warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
322 my $error = $queue->depend_insert($depend_jobnum);
324 $dbh->rollback if $oldAutoCommit;
325 return "error queuing job dependancy: $error";
332 my $nms_ip_error = $self->nms_ip_insert;
333 if ( $nms_ip_error ) {
334 $dbh->rollback if $oldAutoCommit;
335 return "error queuing IP insert: $nms_ip_error";
338 if ( exists $options{'jobnums'} ) {
339 push @{ $options{'jobnums'} }, @jobnums;
342 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
348 sub preinsert_hook_first { ''; }
349 sub _check_duplcate { ''; }
350 sub preinsert_hook { ''; }
351 sub table_dupcheck_fields { (); }
352 sub predelete_hook { ''; }
353 sub predelete_hook_first { ''; }
355 =item delete [ , OPTION => VALUE ... ]
357 Deletes this account from the database. If there is an error, returns the
358 error, otherwise returns false.
360 The corresponding FS::cust_svc record will be deleted as well.
367 my $export_args = $options{'export_args'} || [];
369 local $SIG{HUP} = 'IGNORE';
370 local $SIG{INT} = 'IGNORE';
371 local $SIG{QUIT} = 'IGNORE';
372 local $SIG{TERM} = 'IGNORE';
373 local $SIG{TSTP} = 'IGNORE';
374 local $SIG{PIPE} = 'IGNORE';
376 my $oldAutoCommit = $FS::UID::AutoCommit;
377 local $FS::UID::AutoCommit = 0;
380 my $error = $self->predelete_hook_first
381 || $self->SUPER::delete
382 || $self->export('delete', @$export_args)
383 || $self->return_inventory
384 || $self->predelete_hook
385 || $self->cust_svc->delete
388 $dbh->rollback if $oldAutoCommit;
392 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
399 Currently this will only run expire exports if any are attached
404 my($self,$date) = (shift,shift);
406 return 'Expire date must be specified' unless $date;
408 local $SIG{HUP} = 'IGNORE';
409 local $SIG{INT} = 'IGNORE';
410 local $SIG{QUIT} = 'IGNORE';
411 local $SIG{TERM} = 'IGNORE';
412 local $SIG{TSTP} = 'IGNORE';
413 local $SIG{PIPE} = 'IGNORE';
415 my $oldAutoCommit = $FS::UID::AutoCommit;
416 local $FS::UID::AutoCommit = 0;
419 my $export_args = [$date];
420 my $error = $self->export('expire', @$export_args);
422 $dbh->rollback if $oldAutoCommit;
426 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
431 =item replace [ OLD_RECORD ] [ HASHREF | OPTION => VALUE ]
433 Replaces OLD_RECORD with this one. If there is an error, returns the error,
434 otherwise returns false.
436 Currently available options are: I<export_args> and I<depend_jobnum>.
438 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
439 jobnums), all provisioning jobs will have a dependancy on the supplied
440 jobnum(s) (they will not run until the specific job(s) complete(s)).
442 If I<export_args> is set to an array reference, the referenced list will be
443 passed to export commands.
450 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
455 ( ref($_[0]) eq 'HASH' )
460 local $FS::queue::jobnums = \@jobnums;
461 warn "[$me] replace: set \$FS::queue::jobnums to $FS::queue::jobnums\n"
463 my $depend_jobnums = $options->{'depend_jobnum'} || [];
464 $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
466 local $SIG{HUP} = 'IGNORE';
467 local $SIG{INT} = 'IGNORE';
468 local $SIG{QUIT} = 'IGNORE';
469 local $SIG{TERM} = 'IGNORE';
470 local $SIG{TSTP} = 'IGNORE';
471 local $SIG{PIPE} = 'IGNORE';
473 my $oldAutoCommit = $FS::UID::AutoCommit;
474 local $FS::UID::AutoCommit = 0;
477 my $error = $new->set_auto_inventory($old);
479 $dbh->rollback if $oldAutoCommit;
483 #redundant, but so any duplicate fields are maniuplated as appropriate
484 # (svc_phone.phonenum)
485 $error = $new->check;
487 $dbh->rollback if $oldAutoCommit;
491 #if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
492 if ( grep { $old->$_ ne $new->$_ } $new->table_dupcheck_fields ) {
494 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
495 $error = $new->_check_duplicate;
497 $dbh->rollback if $oldAutoCommit;
502 $error = $new->SUPER::replace($old);
504 $dbh->rollback if $oldAutoCommit;
509 unless ( $noexport_hack ) {
511 warn "[$me] replace: \$FS::queue::jobnums is $FS::queue::jobnums\n"
514 my $export_args = $options->{'export_args'} || [];
516 #not quite false laziness, but same pattern as FS::svc_acct::replace and
517 #FS::part_export::sqlradius::_export_replace. List::Compare or something
518 #would be useful but too much of a pain in the ass to deploy
520 my @old_part_export = $old->cust_svc->part_svc->part_export;
521 my %old_exportnum = map { $_->exportnum => 1 } @old_part_export;
522 my @new_part_export =
524 ? qsearchs('part_svc', { svcpart=>$new->svcpart } )->part_export
525 : $new->cust_svc->part_svc->part_export;
526 my %new_exportnum = map { $_->exportnum => 1 } @new_part_export;
528 foreach my $delete_part_export (
529 grep { ! $new_exportnum{$_->exportnum} } @old_part_export
531 my $error = $delete_part_export->export_delete($old, @$export_args);
533 $dbh->rollback if $oldAutoCommit;
534 return "error deleting, export to ". $delete_part_export->exporttype.
535 " (transaction rolled back): $error";
539 foreach my $replace_part_export (
540 grep { $old_exportnum{$_->exportnum} } @new_part_export
543 $replace_part_export->export_replace( $new, $old, @$export_args);
545 $dbh->rollback if $oldAutoCommit;
546 return "error exporting to ". $replace_part_export->exporttype.
547 " (transaction rolled back): $error";
551 foreach my $insert_part_export (
552 grep { ! $old_exportnum{$_->exportnum} } @new_part_export
554 my $error = $insert_part_export->export_insert($new, @$export_args );
556 $dbh->rollback if $oldAutoCommit;
557 return "error inserting export to ". $insert_part_export->exporttype.
558 " (transaction rolled back): $error";
562 foreach my $depend_jobnum ( @$depend_jobnums ) {
563 warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
565 foreach my $jobnum ( @jobnums ) {
566 my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
567 warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
569 my $error = $queue->depend_insert($depend_jobnum);
571 $dbh->rollback if $oldAutoCommit;
572 return "error queuing job dependancy: $error";
579 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
585 Sets any fixed fields for this service (see L<FS::part_svc>). If there is an
586 error, returns the error, otherwise returns the FS::part_svc object (use ref()
587 to test the return). Usually called by the check method.
593 $self->setx('F', @_);
598 Sets all fields to their defaults (see L<FS::part_svc>), overriding their
599 current values. If there is an error, returns the error, otherwise returns
600 the FS::part_svc object (use ref() to test the return).
606 $self->setx('D', @_ );
609 =item set_default_and_fixed
613 sub set_default_and_fixed {
615 $self->setx( [ 'D', 'F' ], @_ );
618 =item setx FLAG | FLAG_ARRAYREF , [ CALLBACK_HASHREF ]
620 Sets fields according to the passed in flag or arrayref of flags.
622 Optionally, a hashref of field names and callback coderefs can be passed.
623 If a coderef exists for a given field name, instead of setting the field,
624 the coderef is called with the column value (part_svc_column.columnvalue)
625 as the single parameter.
632 my @x = ref($x) ? @$x : ($x);
633 my $coderef = scalar(@_) ? shift : $self->_fieldhandlers;
636 $self->ut_numbern('svcnum')
638 return $error if $error;
640 my $part_svc = $self->part_svc;
641 return "Unknown svcpart" unless $part_svc;
643 #set default/fixed/whatever fields from part_svc
645 foreach my $part_svc_column (
646 grep { my $f = $_->columnflag; grep { $f eq $_ } @x } #columnflag in @x
647 $part_svc->all_part_svc_column
650 my $columnname = $part_svc_column->columnname;
651 my $columnvalue = $part_svc_column->columnvalue;
653 $columnvalue = &{ $coderef->{$columnname} }( $self, $columnvalue )
654 if exists( $coderef->{$columnname} );
655 $self->setfield( $columnname, $columnvalue );
668 if ( $self->get('svcpart') ) {
669 $svcpart = $self->get('svcpart');
670 } elsif ( $self->svcnum && qsearchs('cust_svc', {'svcnum'=>$self->svcnum}) ) {
671 my $cust_svc = $self->cust_svc;
672 return "Unknown svcnum" unless $cust_svc;
673 $svcpart = $cust_svc->svcpart;
676 qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
682 Returns the FS::svc_pbx record for this service, if any (see L<FS::svc_pbx>).
684 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
689 # XXX FS::h_svc_{acct,phone} could have a history-aware svc_pbx override
693 return '' unless $self->pbxsvc;
694 qsearchs( 'svc_pbx', { 'svcnum' => $self->pbxsvc } );
699 Returns the title of the FS::svc_pbx record associated with this service, if
702 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
709 my $svc_pbx = $self->svc_pbx or return '';
713 =item pbx_select_hash %OPTIONS
715 Can be called as an object method or a class method.
717 Returns a hash SVCNUM => TITLE ... representing the PBXes this customer
718 that may be associated with this service.
720 Currently available options are: I<pkgnum> I<svcpart>
722 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
727 #false laziness w/svc_acct::domain_select_hash
728 sub pbx_select_hash {
729 my ($self, %options) = @_;
735 $part_svc = $self->part_svc;
736 $cust_pkg = $self->cust_svc->cust_pkg
740 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
741 if $options{'svcpart'};
743 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
744 if $options{'pkgnum'};
746 if ($part_svc && ( $part_svc->part_svc_column('pbxsvc')->columnflag eq 'S'
747 || $part_svc->part_svc_column('pbxsvc')->columnflag eq 'F')) {
748 %pbxes = map { $_->svcnum => $_->title }
749 map { qsearchs('svc_pbx', { 'svcnum' => $_ }) }
750 split(',', $part_svc->part_svc_column('pbxsvc')->columnvalue);
751 } elsif ($cust_pkg) { # && !$conf->exists('svc_acct-alldomains') ) {
752 %pbxes = map { $_->svcnum => $_->title }
753 map { qsearchs('svc_pbx', { 'svcnum' => $_->svcnum }) }
754 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
755 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
758 %pbxes = map { $_->svcnum => $_->title } qsearch('svc_pbx', {} );
761 if ($part_svc && $part_svc->part_svc_column('pbxsvc')->columnflag eq 'D') {
762 my $svc_pbx = qsearchs('svc_pbx',
763 { 'svcnum' => $part_svc->part_svc_column('pbxsvc')->columnvalue } );
765 $pbxes{$svc_pbx->svcnum} = $svc_pbx->title;
767 warn "unknown svc_pbx.svcnum for part_svc_column pbxsvc: ".
768 $part_svc->part_svc_column('pbxsvc')->columnvalue;
777 =item set_auto_inventory
779 Sets any fields which auto-populate from inventory (see L<FS::part_svc>), and
780 also check any manually populated inventory fields.
782 If there is an error, returns the error, otherwise returns false.
786 sub set_auto_inventory {
788 my $old = @_ ? shift : '';
791 $self->ut_numbern('svcnum')
793 return $error if $error;
795 my $part_svc = $self->part_svc;
796 return "Unkonwn svcpart" unless $part_svc;
798 local $SIG{HUP} = 'IGNORE';
799 local $SIG{INT} = 'IGNORE';
800 local $SIG{QUIT} = 'IGNORE';
801 local $SIG{TERM} = 'IGNORE';
802 local $SIG{TSTP} = 'IGNORE';
803 local $SIG{PIPE} = 'IGNORE';
805 my $oldAutoCommit = $FS::UID::AutoCommit;
806 local $FS::UID::AutoCommit = 0;
809 #set default/fixed/whatever fields from part_svc
810 my $table = $self->table;
811 foreach my $field ( grep { $_ ne 'svcnum' } $self->fields ) {
813 my $part_svc_column = $part_svc->part_svc_column($field);
814 my $columnflag = $part_svc_column->columnflag;
815 next unless $columnflag =~ /^[AM]$/;
817 next if $columnflag eq 'A' && $self->$field() ne '';
819 my $classnum = $part_svc_column->columnvalue;
820 my %hash = ( 'classnum' => $classnum );
822 if ( $columnflag eq 'A' && $self->$field() eq '' ) {
823 $hash{'svcnum'} = '';
824 } elsif ( $columnflag eq 'M' ) {
825 return "Select inventory item for $field" unless $self->getfield($field);
826 $hash{'item'} = $self->getfield($field);
829 my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql(
831 'table' => 'inventory_item',
834 my $inventory_item = qsearchs({
835 'table' => 'inventory_item',
837 'extra_sql' => "AND $agentnums_sql",
838 'order_by' => 'ORDER BY ( agentnum IS NULL ) '. #agent inventory first
839 ' LIMIT 1 FOR UPDATE',
842 unless ( $inventory_item ) {
843 $dbh->rollback if $oldAutoCommit;
844 my $inventory_class =
845 qsearchs('inventory_class', { 'classnum' => $classnum } );
846 return "Can't find inventory_class.classnum $classnum"
847 unless $inventory_class;
848 return "Out of ". PL_N($inventory_class->classname);
851 next if $columnflag eq 'M' && $inventory_item->svcnum == $self->svcnum;
853 $self->setfield( $field, $inventory_item->item );
854 #if $columnflag eq 'A' && $self->$field() eq '';
856 if ( $old && $old->$field() && $old->$field() ne $self->$field() ) {
857 my $old_inv = qsearchs({
858 'table' => 'inventory_item',
859 'hashref' => { 'classnum' => $classnum,
860 'svcnum' => $old->svcnum,
862 'extra_sql' => ' AND '.
863 '( ( svc_field IS NOT NULL AND svc_field = '.$dbh->quote($field).' )'.
864 ' OR ( svc_field IS NULL AND item = '. dbh->quote($old->$field).' )'.
868 $old_inv->svcnum('');
869 $old_inv->svc_field('');
870 my $oerror = $old_inv->replace;
872 $dbh->rollback if $oldAutoCommit;
873 return "Error unprovisioning inventory: $oerror";
876 warn "old inventory_item not found for $field ". $self->$field;
880 $inventory_item->svcnum( $self->svcnum );
881 $inventory_item->svc_field( $field );
882 my $ierror = $inventory_item->replace();
884 $dbh->rollback if $oldAutoCommit;
885 return "Error provisioning inventory: $ierror";
890 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
896 =item return_inventory
900 sub return_inventory {
903 local $SIG{HUP} = 'IGNORE';
904 local $SIG{INT} = 'IGNORE';
905 local $SIG{QUIT} = 'IGNORE';
906 local $SIG{TERM} = 'IGNORE';
907 local $SIG{TSTP} = 'IGNORE';
908 local $SIG{PIPE} = 'IGNORE';
910 my $oldAutoCommit = $FS::UID::AutoCommit;
911 local $FS::UID::AutoCommit = 0;
914 foreach my $inventory_item ( $self->inventory_item ) {
915 $inventory_item->svcnum('');
916 $inventory_item->svc_field('');
917 my $error = $inventory_item->replace();
919 $dbh->rollback if $oldAutoCommit;
920 return "Error returning inventory: $error";
924 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
931 Returns the inventory items associated with this svc_ record, as
932 FS::inventory_item objects (see L<FS::inventory_item>.
939 'table' => 'inventory_item',
940 'hashref' => { 'svcnum' => $self->svcnum, },
946 Returns the cust_svc record associated with this svc_ record, as a FS::cust_svc
947 object (see L<FS::cust_svc>).
953 qsearchs('cust_svc', { 'svcnum' => $self->svcnum } );
958 Runs export_suspend callbacks.
965 my $export_args = $options{'export_args'} || [];
966 $self->export('suspend', @$export_args);
971 Runs export_unsuspend callbacks.
978 my $export_args = $options{'export_args'} || [];
979 $self->export('unsuspend', @$export_args);
984 Runs export_links callbacks and returns the links.
991 $self->export('links', $return);
995 =item export_getsettings
997 Runs export_getsettings callbacks and returns the two hashrefs.
1001 sub export_getsettings {
1005 my $error = $self->export('getsettings', \%settings, \%defaults);
1007 warn "error running export_getsetings: $error";
1008 return ( { 'error' => $error }, {} );
1010 ( \%settings, \%defaults );
1013 =item export_getstatus
1015 Runs export_getstatus callbacks and returns a two item list consisting of an
1016 HTML status and a status hashref.
1020 sub export_getstatus {
1024 my $error = $self->export('getstatus', \$html, \%hash);
1026 warn "error running export_getstatus: $error";
1027 return ( '', { 'error' => $error } );
1032 =item export_setstatus
1034 Runs export_setstatus callbacks. If there is an error, returns the error,
1035 otherwise returns false.
1039 sub export_setstatus {
1040 my( $self, @args ) = @_;
1041 my $error = $self->export('setstatus', @args);
1043 warn "error running export_setstatus: $error";
1050 =item export HOOK [ EXPORT_ARGS ]
1052 Runs the provided export hook (i.e. "suspend", "unsuspend") for this service.
1057 my( $self, $method ) = ( shift, shift );
1059 $method = "export_$method" unless $method =~ /^export_/;
1061 local $SIG{HUP} = 'IGNORE';
1062 local $SIG{INT} = 'IGNORE';
1063 local $SIG{QUIT} = 'IGNORE';
1064 local $SIG{TERM} = 'IGNORE';
1065 local $SIG{TSTP} = 'IGNORE';
1066 local $SIG{PIPE} = 'IGNORE';
1068 my $oldAutoCommit = $FS::UID::AutoCommit;
1069 local $FS::UID::AutoCommit = 0;
1073 unless ( $noexport_hack ) {
1074 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1075 next unless $part_export->can($method);
1076 my $error = $part_export->$method($self, @_);
1078 $dbh->rollback if $oldAutoCommit;
1079 return "error exporting $method event to ". $part_export->exporttype.
1080 " (transaction rolled back): $error";
1085 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1092 Sets or retrieves overlimit date.
1098 #$self->cust_svc->overlimit(@_);
1099 my $cust_svc = $self->cust_svc;
1100 unless ( $cust_svc ) { #wtf?
1101 my $error = "$me overlimit: missing cust_svc record for svc_acct svcnum ".
1103 if ( $overlimit_missing_cust_svc_nonfatal_kludge ) {
1104 cluck "$error; continuing anyway as requested";
1110 $cust_svc->overlimit(@_);
1115 Stub - returns false (no error) so derived classes don't need to define this
1116 methods. Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
1118 This method is called *before* the deletion step which actually deletes the
1119 services. This method should therefore only be used for "pre-deletion"
1120 cancellation steps, if necessary.
1126 =item clone_suspended
1128 Constructor used by FS::part_export::_export_suspend fallback. Stub returning
1129 same object for svc_ classes which don't implement a suspension fallback
1130 (everything except svc_acct at the moment). Document better.
1134 sub clone_suspended {
1138 =item clone_kludge_unsuspend
1140 Constructor used by FS::part_export::_export_unsuspend fallback. Stub returning
1141 same object for svc_ classes which don't implement a suspension fallback
1142 (everything except svc_acct at the moment). Document better.
1146 sub clone_kludge_unsuspend {
1150 =item find_duplicates MODE FIELDS...
1152 Method used by _check_duplicate routines to find services with duplicate
1153 values in specified fields. Set MODE to 'global' to search across all
1154 services, or 'export' to limit to those that share one or more exports
1155 with this service. FIELDS is a list of field names; only services
1156 matching in all fields will be returned. Empty fields will be skipped.
1160 sub find_duplicates {
1165 my %search = map { $_ => $self->getfield($_) }
1166 grep { length($self->getfield($_)) } @fields;
1167 return () if !%search;
1168 my @dup = grep { ! $self->svcnum or $_->svcnum != $self->svcnum }
1169 qsearch( $self->table, \%search );
1171 return @dup if $mode eq 'global';
1172 die "incorrect find_duplicates mode '$mode'" if $mode ne 'export';
1174 my $exports = FS::part_export::export_info($self->table);
1175 my %conflict_svcparts;
1176 my $part_svc = $self->part_svc;
1177 foreach my $part_export ( $part_svc->part_export ) {
1178 %conflict_svcparts = map { $_->svcpart => 1 } $part_export->export_svc;
1180 return grep { $conflict_svcparts{$_->cust_svc->svcpart} } @dup;
1183 =item getstatus_html
1187 sub getstatus_html {
1190 my $part_svc = $self->cust_svc->part_svc;
1194 foreach my $export ( grep $_->can('export_getstatus'), $part_svc->part_export ) {
1195 my $export_html = '';
1197 $export->export_getstatus( $self, \$export_html, \%hash );
1198 $html .= $export_html;
1211 my $conf = new FS::Conf;
1212 return '' unless grep { $self->table eq $_ }
1213 $conf->config('nms-auto_add-svc_ips');
1214 my $ip_field = $self->table_info->{'ip_field'};
1216 my $queue = FS::queue->new( {
1217 'job' => 'FS::NetworkMonitoringSystem::queued_add_router',
1218 'svcnum' => $self->svcnum,
1220 $queue->insert( 'FS::NetworkMonitoringSystem',
1222 $conf->config('nms-auto_add-community')
1231 #XXX not yet implemented
1238 The setfixed method return value.
1240 B<export> method isn't used by insert and replace methods yet.
1244 L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, schema.html
1245 from the base documentation.