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 );
9 use FS::Record qw( qsearch qsearchs fields dbh );
10 use FS::cust_main_Mixin;
15 use FS::inventory_item;
16 use FS::inventory_class;
17 use FS::NetworkMonitoringSystem;
19 @ISA = qw( FS::cust_main_Mixin FS::Record );
21 $me = '[FS::svc_Common]';
24 $overlimit_missing_cust_svc_nonfatal_kludge = 0;
28 FS::svc_Common - Object method for all svc_ records
34 @ISA = qw( FS::svc_Common );
38 FS::svc_Common is intended as a base class for table-specific classes to
39 inherit from, i.e. FS::svc_acct. FS::svc_Common inherits from FS::Record.
45 =item search_sql_field FIELD STRING
47 Class method which returns an SQL fragment to search for STRING in FIELD.
49 It is now case-insensitive by default.
53 sub search_sql_field {
54 my( $class, $field, $string ) = @_;
55 my $table = $class->table;
56 my $q_string = dbh->quote($string);
57 "LOWER($table.$field) = LOWER($q_string)";
60 #fallback for services that don't provide a search...
62 #my( $class, $string ) = @_;
72 my $class = ref($proto) || $proto;
74 bless ($self, $class);
76 unless ( defined ( $self->table ) ) {
77 $self->{'Table'} = shift;
78 carp "warning: FS::Record::new called with table name ". $self->{'Table'};
81 #$self->{'Hash'} = shift;
83 $self->{'Hash'} = { map { $_ => $newhash->{$_} } qw(svcnum svcpart) };
85 $self->setdefault( $self->_fieldhandlers )
88 $self->{'Hash'}{$_} = $newhash->{$_}
89 foreach grep { defined($newhash->{$_}) && length($newhash->{$_}) }
92 foreach my $field ( grep !defined($self->{'Hash'}{$_}), $self->fields ) {
93 $self->{'Hash'}{$field}='';
96 $self->_rebless if $self->can('_rebless');
98 $self->{'modified'} = 0;
100 $self->_cache($self->{'Hash'}, shift) if $self->can('_cache') && @_;
106 sub _fieldhandlers { {}; }
110 # This restricts the fields based on part_svc_column and the svcpart of
111 # the service. There are four possible cases:
112 # 1. svcpart passed as part of the svc_x hash.
113 # 2. svcpart fetched via cust_svc based on svcnum.
114 # 3. No svcnum or svcpart. In this case, return ALL the fields with
115 # dbtable eq $self->table.
116 # 4. Called via "fields('svc_acct')" or something similar. In this case
117 # there is no $self object.
121 my @vfields = $self->SUPER::virtual_fields;
123 return @vfields unless (ref $self); # Case 4
125 if ($self->svcpart) { # Case 1
126 $svcpart = $self->svcpart;
127 } elsif ( $self->svcnum
128 && qsearchs('cust_svc',{'svcnum'=>$self->svcnum} )
130 $svcpart = $self->cust_svc->svcpart;
135 if ($svcpart) { #Cases 1 and 2
136 my %flags = map { $_->columnname, $_->columnflag } (
137 qsearch ('part_svc_column', { svcpart => $svcpart } )
139 return grep { not ( defined($flags{$_}) && $flags{$_} eq 'X') } @vfields;
148 svc_Common provides a fallback label subroutine that just returns the svcnum.
154 cluck "warning: ". ref($self). " not loaded or missing label method; ".
166 Checks the validity of fields in this record.
168 At present, this does nothing but call FS::Record::check (which, in turn,
169 does nothing but run virtual field checks).
178 =item insert [ , OPTION => VALUE ... ]
180 Adds this record to the database. If there is an error, returns the error,
181 otherwise returns false.
183 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
184 defined. An FS::cust_svc record will be created and inserted.
186 Currently available options are: I<jobnums>, I<child_objects> and
189 If I<jobnum> is set to an array reference, the jobnums of any export jobs will
190 be added to the referenced array.
192 If I<child_objects> is set to an array reference of FS::tablename objects (for
193 example, FS::acct_snarf objects), they will have their svcnum field set and
194 will be inserted after this record, but before any exports are run. Each
195 element of the array can also optionally be a two-element array reference
196 containing the child object and the name of an alternate field to be filled in
197 with the newly-inserted svcnum, for example C<[ $svc_forward, 'srcsvc' ]>
199 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
200 jobnums), all provisioning jobs will have a dependancy on the supplied
201 jobnum(s) (they will not run until the specific job(s) complete(s)).
203 If I<export_args> is set to an array reference, the referenced list will be
204 passed to export commands.
211 warn "[$me] insert called with options ".
212 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
216 local $FS::queue::jobnums = \@jobnums;
217 warn "[$me] insert: set \$FS::queue::jobnums to $FS::queue::jobnums\n"
219 my $objects = $options{'child_objects'} || [];
220 my $depend_jobnums = $options{'depend_jobnum'} || [];
221 $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
223 local $SIG{HUP} = 'IGNORE';
224 local $SIG{INT} = 'IGNORE';
225 local $SIG{QUIT} = 'IGNORE';
226 local $SIG{TERM} = 'IGNORE';
227 local $SIG{TSTP} = 'IGNORE';
228 local $SIG{PIPE} = 'IGNORE';
230 my $oldAutoCommit = $FS::UID::AutoCommit;
231 local $FS::UID::AutoCommit = 0;
234 my $svcnum = $self->svcnum;
235 my $cust_svc = $svcnum ? qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) : '';
236 #unless ( $svcnum ) {
237 if ( !$svcnum or !$cust_svc ) {
238 $cust_svc = new FS::cust_svc ( {
239 #hua?# 'svcnum' => $svcnum,
240 'svcnum' => $self->svcnum,
241 'pkgnum' => $self->pkgnum,
242 'svcpart' => $self->svcpart,
244 my $error = $cust_svc->insert;
246 $dbh->rollback if $oldAutoCommit;
249 $svcnum = $self->svcnum($cust_svc->svcnum);
251 #$cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
252 unless ( $cust_svc ) {
253 $dbh->rollback if $oldAutoCommit;
254 return "no cust_svc record found for svcnum ". $self->svcnum;
256 $self->pkgnum($cust_svc->pkgnum);
257 $self->svcpart($cust_svc->svcpart);
260 my $error = $self->preinsert_hook_first
261 || $self->set_auto_inventory
263 || $self->_check_duplicate
264 || $self->preinsert_hook
265 || $self->SUPER::insert;
267 $dbh->rollback if $oldAutoCommit;
271 foreach my $object ( @$objects ) {
273 if ( ref($object) eq 'ARRAY' ) {
274 ($obj, $field) = @$object;
279 $obj->$field($self->svcnum);
280 $error = $obj->insert;
282 $dbh->rollback if $oldAutoCommit;
288 unless ( $noexport_hack ) {
290 warn "[$me] insert: \$FS::queue::jobnums is $FS::queue::jobnums\n"
293 my $export_args = $options{'export_args'} || [];
295 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
296 my $error = $part_export->export_insert($self, @$export_args);
298 $dbh->rollback if $oldAutoCommit;
299 return "exporting to ". $part_export->exporttype.
300 " (transaction rolled back): $error";
304 foreach my $depend_jobnum ( @$depend_jobnums ) {
305 warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
307 foreach my $jobnum ( @jobnums ) {
308 my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
309 warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
311 my $error = $queue->depend_insert($depend_jobnum);
313 $dbh->rollback if $oldAutoCommit;
314 return "error queuing job dependancy: $error";
321 $self->nms_ip_insert;
323 if ( exists $options{'jobnums'} ) {
324 push @{ $options{'jobnums'} }, @jobnums;
327 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
333 sub preinsert_hook_first { ''; }
334 sub _check_duplcate { ''; }
335 sub preinsert_hook { ''; }
336 sub table_dupcheck_fields { (); }
337 sub predelete_hook { ''; }
338 sub predelete_hook_first { ''; }
340 =item delete [ , OPTION => VALUE ... ]
342 Deletes this account from the database. If there is an error, returns the
343 error, otherwise returns false.
345 The corresponding FS::cust_svc record will be deleted as well.
352 my $export_args = $options{'export_args'} || [];
354 local $SIG{HUP} = 'IGNORE';
355 local $SIG{INT} = 'IGNORE';
356 local $SIG{QUIT} = 'IGNORE';
357 local $SIG{TERM} = 'IGNORE';
358 local $SIG{TSTP} = 'IGNORE';
359 local $SIG{PIPE} = 'IGNORE';
361 my $oldAutoCommit = $FS::UID::AutoCommit;
362 local $FS::UID::AutoCommit = 0;
365 my $error = $self->predelete_hook_first
366 || $self->SUPER::delete
367 || $self->export('delete', @$export_args)
368 || $self->return_inventory
369 || $self->predelete_hook
370 || $self->cust_svc->delete
373 $dbh->rollback if $oldAutoCommit;
377 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
384 Currently this will only run expire exports if any are attached
389 my($self,$date) = (shift,shift);
391 return 'Expire date must be specified' unless $date;
393 local $SIG{HUP} = 'IGNORE';
394 local $SIG{INT} = 'IGNORE';
395 local $SIG{QUIT} = 'IGNORE';
396 local $SIG{TERM} = 'IGNORE';
397 local $SIG{TSTP} = 'IGNORE';
398 local $SIG{PIPE} = 'IGNORE';
400 my $oldAutoCommit = $FS::UID::AutoCommit;
401 local $FS::UID::AutoCommit = 0;
404 my $export_args = [$date];
405 my $error = $self->export('expire', @$export_args);
407 $dbh->rollback if $oldAutoCommit;
411 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
416 =item replace [ OLD_RECORD ] [ HASHREF | OPTION => VALUE ]
418 Replaces OLD_RECORD with this one. If there is an error, returns the error,
419 otherwise returns false.
421 Currently available options are: I<export_args> and I<depend_jobnum>.
423 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
424 jobnums), all provisioning jobs will have a dependancy on the supplied
425 jobnum(s) (they will not run until the specific job(s) complete(s)).
427 If I<export_args> is set to an array reference, the referenced list will be
428 passed to export commands.
435 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
440 ( ref($_[0]) eq 'HASH' )
445 local $FS::queue::jobnums = \@jobnums;
446 warn "[$me] replace: set \$FS::queue::jobnums to $FS::queue::jobnums\n"
448 my $depend_jobnums = $options->{'depend_jobnum'} || [];
449 $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
451 local $SIG{HUP} = 'IGNORE';
452 local $SIG{INT} = 'IGNORE';
453 local $SIG{QUIT} = 'IGNORE';
454 local $SIG{TERM} = 'IGNORE';
455 local $SIG{TSTP} = 'IGNORE';
456 local $SIG{PIPE} = 'IGNORE';
458 my $oldAutoCommit = $FS::UID::AutoCommit;
459 local $FS::UID::AutoCommit = 0;
462 my $error = $new->set_auto_inventory($old);
464 $dbh->rollback if $oldAutoCommit;
468 #redundant, but so any duplicate fields are maniuplated as appropriate
469 # (svc_phone.phonenum)
470 $error = $new->check;
472 $dbh->rollback if $oldAutoCommit;
476 #if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
477 if ( grep { $old->$_ ne $new->$_ } $new->table_dupcheck_fields ) {
479 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
480 $error = $new->_check_duplicate;
482 $dbh->rollback if $oldAutoCommit;
487 $error = $new->SUPER::replace($old);
489 $dbh->rollback if $oldAutoCommit;
494 unless ( $noexport_hack ) {
496 warn "[$me] replace: \$FS::queue::jobnums is $FS::queue::jobnums\n"
499 my $export_args = $options->{'export_args'} || [];
501 #not quite false laziness, but same pattern as FS::svc_acct::replace and
502 #FS::part_export::sqlradius::_export_replace. List::Compare or something
503 #would be useful but too much of a pain in the ass to deploy
505 my @old_part_export = $old->cust_svc->part_svc->part_export;
506 my %old_exportnum = map { $_->exportnum => 1 } @old_part_export;
507 my @new_part_export =
509 ? qsearchs('part_svc', { svcpart=>$new->svcpart } )->part_export
510 : $new->cust_svc->part_svc->part_export;
511 my %new_exportnum = map { $_->exportnum => 1 } @new_part_export;
513 foreach my $delete_part_export (
514 grep { ! $new_exportnum{$_->exportnum} } @old_part_export
516 my $error = $delete_part_export->export_delete($old, @$export_args);
518 $dbh->rollback if $oldAutoCommit;
519 return "error deleting, export to ". $delete_part_export->exporttype.
520 " (transaction rolled back): $error";
524 foreach my $replace_part_export (
525 grep { $old_exportnum{$_->exportnum} } @new_part_export
528 $replace_part_export->export_replace( $new, $old, @$export_args);
530 $dbh->rollback if $oldAutoCommit;
531 return "error exporting to ". $replace_part_export->exporttype.
532 " (transaction rolled back): $error";
536 foreach my $insert_part_export (
537 grep { ! $old_exportnum{$_->exportnum} } @new_part_export
539 my $error = $insert_part_export->export_insert($new, @$export_args );
541 $dbh->rollback if $oldAutoCommit;
542 return "error inserting export to ". $insert_part_export->exporttype.
543 " (transaction rolled back): $error";
547 foreach my $depend_jobnum ( @$depend_jobnums ) {
548 warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
550 foreach my $jobnum ( @jobnums ) {
551 my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
552 warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
554 my $error = $queue->depend_insert($depend_jobnum);
556 $dbh->rollback if $oldAutoCommit;
557 return "error queuing job dependancy: $error";
564 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
570 Sets any fixed fields for this service (see L<FS::part_svc>). If there is an
571 error, returns the error, otherwise returns the FS::part_svc object (use ref()
572 to test the return). Usually called by the check method.
578 $self->setx('F', @_);
583 Sets all fields to their defaults (see L<FS::part_svc>), overriding their
584 current values. If there is an error, returns the error, otherwise returns
585 the FS::part_svc object (use ref() to test the return).
591 $self->setx('D', @_ );
594 =item set_default_and_fixed
598 sub set_default_and_fixed {
600 $self->setx( [ 'D', 'F' ], @_ );
603 =item setx FLAG | FLAG_ARRAYREF , [ CALLBACK_HASHREF ]
605 Sets fields according to the passed in flag or arrayref of flags.
607 Optionally, a hashref of field names and callback coderefs can be passed.
608 If a coderef exists for a given field name, instead of setting the field,
609 the coderef is called with the column value (part_svc_column.columnvalue)
610 as the single parameter.
617 my @x = ref($x) ? @$x : ($x);
618 my $coderef = scalar(@_) ? shift : $self->_fieldhandlers;
621 $self->ut_numbern('svcnum')
623 return $error if $error;
625 my $part_svc = $self->part_svc;
626 return "Unknown svcpart" unless $part_svc;
628 #set default/fixed/whatever fields from part_svc
630 foreach my $part_svc_column (
631 grep { my $f = $_->columnflag; grep { $f eq $_ } @x } #columnflag in @x
632 $part_svc->all_part_svc_column
635 my $columnname = $part_svc_column->columnname;
636 my $columnvalue = $part_svc_column->columnvalue;
638 $columnvalue = &{ $coderef->{$columnname} }( $self, $columnvalue )
639 if exists( $coderef->{$columnname} );
640 $self->setfield( $columnname, $columnvalue );
653 if ( $self->get('svcpart') ) {
654 $svcpart = $self->get('svcpart');
655 } elsif ( $self->svcnum && qsearchs('cust_svc', {'svcnum'=>$self->svcnum}) ) {
656 my $cust_svc = $self->cust_svc;
657 return "Unknown svcnum" unless $cust_svc;
658 $svcpart = $cust_svc->svcpart;
661 qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
667 Returns the FS::svc_pbx record for this service, if any (see L<FS::svc_pbx>).
669 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
674 # XXX FS::h_svc_{acct,phone} could have a history-aware svc_pbx override
678 return '' unless $self->pbxsvc;
679 qsearchs( 'svc_pbx', { 'svcnum' => $self->pbxsvc } );
684 Returns the title of the FS::svc_pbx record associated with this service, if
687 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
694 my $svc_pbx = $self->svc_pbx or return '';
698 =item pbx_select_hash %OPTIONS
700 Can be called as an object method or a class method.
702 Returns a hash SVCNUM => TITLE ... representing the PBXes this customer
703 that may be associated with this service.
705 Currently available options are: I<pkgnum> I<svcpart>
707 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
712 #false laziness w/svc_acct::domain_select_hash
713 sub pbx_select_hash {
714 my ($self, %options) = @_;
720 $part_svc = $self->part_svc;
721 $cust_pkg = $self->cust_svc->cust_pkg
725 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
726 if $options{'svcpart'};
728 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
729 if $options{'pkgnum'};
731 if ($part_svc && ( $part_svc->part_svc_column('pbxsvc')->columnflag eq 'S'
732 || $part_svc->part_svc_column('pbxsvc')->columnflag eq 'F')) {
733 %pbxes = map { $_->svcnum => $_->title }
734 map { qsearchs('svc_pbx', { 'svcnum' => $_ }) }
735 split(',', $part_svc->part_svc_column('pbxsvc')->columnvalue);
736 } elsif ($cust_pkg) { # && !$conf->exists('svc_acct-alldomains') ) {
737 %pbxes = map { $_->svcnum => $_->title }
738 map { qsearchs('svc_pbx', { 'svcnum' => $_->svcnum }) }
739 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
740 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
743 %pbxes = map { $_->svcnum => $_->title } qsearch('svc_pbx', {} );
746 if ($part_svc && $part_svc->part_svc_column('pbxsvc')->columnflag eq 'D') {
747 my $svc_pbx = qsearchs('svc_pbx',
748 { 'svcnum' => $part_svc->part_svc_column('pbxsvc')->columnvalue } );
750 $pbxes{$svc_pbx->svcnum} = $svc_pbx->title;
752 warn "unknown svc_pbx.svcnum for part_svc_column pbxsvc: ".
753 $part_svc->part_svc_column('pbxsvc')->columnvalue;
762 =item set_auto_inventory
764 Sets any fields which auto-populate from inventory (see L<FS::part_svc>), and
765 also check any manually populated inventory fields.
767 If there is an error, returns the error, otherwise returns false.
771 sub set_auto_inventory {
773 my $old = @_ ? shift : '';
776 $self->ut_numbern('svcnum')
778 return $error if $error;
780 my $part_svc = $self->part_svc;
781 return "Unkonwn svcpart" unless $part_svc;
783 local $SIG{HUP} = 'IGNORE';
784 local $SIG{INT} = 'IGNORE';
785 local $SIG{QUIT} = 'IGNORE';
786 local $SIG{TERM} = 'IGNORE';
787 local $SIG{TSTP} = 'IGNORE';
788 local $SIG{PIPE} = 'IGNORE';
790 my $oldAutoCommit = $FS::UID::AutoCommit;
791 local $FS::UID::AutoCommit = 0;
794 #set default/fixed/whatever fields from part_svc
795 my $table = $self->table;
796 foreach my $field ( grep { $_ ne 'svcnum' } $self->fields ) {
798 my $part_svc_column = $part_svc->part_svc_column($field);
799 my $columnflag = $part_svc_column->columnflag;
800 next unless $columnflag =~ /^[AM]$/;
802 next if $columnflag eq 'A' && $self->$field() ne '';
804 my $classnum = $part_svc_column->columnvalue;
805 my %hash = ( 'classnum' => $classnum );
807 if ( $columnflag eq 'A' && $self->$field() eq '' ) {
808 $hash{'svcnum'} = '';
809 } elsif ( $columnflag eq 'M' ) {
810 return "Select inventory item for $field" unless $self->getfield($field);
811 $hash{'item'} = $self->getfield($field);
814 my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql(
816 'table' => 'inventory_item',
819 my $inventory_item = qsearchs({
820 'table' => 'inventory_item',
822 'extra_sql' => "AND $agentnums_sql",
823 'order_by' => 'ORDER BY ( agentnum IS NULL ) '. #agent inventory first
824 ' LIMIT 1 FOR UPDATE',
827 unless ( $inventory_item ) {
828 $dbh->rollback if $oldAutoCommit;
829 my $inventory_class =
830 qsearchs('inventory_class', { 'classnum' => $classnum } );
831 return "Can't find inventory_class.classnum $classnum"
832 unless $inventory_class;
833 return "Out of ". $inventory_class->classname. "s\n"; #Lingua:: BS
837 next if $columnflag eq 'M' && $inventory_item->svcnum == $self->svcnum;
839 $self->setfield( $field, $inventory_item->item );
840 #if $columnflag eq 'A' && $self->$field() eq '';
842 $inventory_item->svcnum( $self->svcnum );
843 my $ierror = $inventory_item->replace();
845 $dbh->rollback if $oldAutoCommit;
846 return "Error provisioning inventory: $ierror";
849 if ( $old && $old->$field() && $old->$field() ne $self->$field() ) {
850 my $old_inv = qsearchs({
851 'table' => 'inventory_item',
852 'hashref' => { 'classnum' => $classnum,
853 'svcnum' => $old->svcnum,
854 'item' => $old->$field(),
858 $old_inv->svcnum('');
859 my $oerror = $old_inv->replace;
861 $dbh->rollback if $oldAutoCommit;
862 return "Error unprovisioning inventory: $oerror";
869 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
875 =item return_inventory
879 sub return_inventory {
882 local $SIG{HUP} = 'IGNORE';
883 local $SIG{INT} = 'IGNORE';
884 local $SIG{QUIT} = 'IGNORE';
885 local $SIG{TERM} = 'IGNORE';
886 local $SIG{TSTP} = 'IGNORE';
887 local $SIG{PIPE} = 'IGNORE';
889 my $oldAutoCommit = $FS::UID::AutoCommit;
890 local $FS::UID::AutoCommit = 0;
893 foreach my $inventory_item ( $self->inventory_item ) {
894 $inventory_item->svcnum('');
895 my $error = $inventory_item->replace();
897 $dbh->rollback if $oldAutoCommit;
898 return "Error returning inventory: $error";
902 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
909 Returns the inventory items associated with this svc_ record, as
910 FS::inventory_item objects (see L<FS::inventory_item>.
917 'table' => 'inventory_item',
918 'hashref' => { 'svcnum' => $self->svcnum, },
924 Returns the cust_svc record associated with this svc_ record, as a FS::cust_svc
925 object (see L<FS::cust_svc>).
931 qsearchs('cust_svc', { 'svcnum' => $self->svcnum } );
936 Runs export_suspend callbacks.
943 my $export_args = $options{'export_args'} || [];
944 $self->export('suspend', @$export_args);
949 Runs export_unsuspend callbacks.
956 my $export_args = $options{'export_args'} || [];
957 $self->export('unsuspend', @$export_args);
962 Runs export_links callbacks and returns the links.
969 $self->export('links', $return);
973 =item export_getsettings
975 Runs export_getsettings callbacks and returns the two hashrefs.
979 sub export_getsettings {
983 my $error = $self->export('getsettings', \%settings, \%defaults);
985 #XXX bubble this up better
986 warn "error running export_getsetings: $error";
989 ( \%settings, \%defaults );
992 =item export_getstatus
994 Runs export_getstatus callbacks and returns a two item list consisting of an
995 HTML status and a status hashref.
999 sub export_getstatus {
1003 my $error = $self->export('getstatus', \$html, \%hash);
1005 #XXX bubble this up better
1006 warn "error running export_getstatus: $error";
1012 =item export HOOK [ EXPORT_ARGS ]
1014 Runs the provided export hook (i.e. "suspend", "unsuspend") for this service.
1019 my( $self, $method ) = ( shift, shift );
1021 $method = "export_$method" unless $method =~ /^export_/;
1023 local $SIG{HUP} = 'IGNORE';
1024 local $SIG{INT} = 'IGNORE';
1025 local $SIG{QUIT} = 'IGNORE';
1026 local $SIG{TERM} = 'IGNORE';
1027 local $SIG{TSTP} = 'IGNORE';
1028 local $SIG{PIPE} = 'IGNORE';
1030 my $oldAutoCommit = $FS::UID::AutoCommit;
1031 local $FS::UID::AutoCommit = 0;
1035 unless ( $noexport_hack ) {
1036 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1037 next unless $part_export->can($method);
1038 my $error = $part_export->$method($self, @_);
1040 $dbh->rollback if $oldAutoCommit;
1041 return "error exporting $method event to ". $part_export->exporttype.
1042 " (transaction rolled back): $error";
1047 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1054 Sets or retrieves overlimit date.
1060 #$self->cust_svc->overlimit(@_);
1061 my $cust_svc = $self->cust_svc;
1062 unless ( $cust_svc ) { #wtf?
1063 my $error = "$me overlimit: missing cust_svc record for svc_acct svcnum ".
1065 if ( $overlimit_missing_cust_svc_nonfatal_kludge ) {
1066 cluck "$error; continuing anyway as requested";
1072 $cust_svc->overlimit(@_);
1077 Stub - returns false (no error) so derived classes don't need to define this
1078 methods. Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
1080 This method is called *before* the deletion step which actually deletes the
1081 services. This method should therefore only be used for "pre-deletion"
1082 cancellation steps, if necessary.
1088 =item clone_suspended
1090 Constructor used by FS::part_export::_export_suspend fallback. Stub returning
1091 same object for svc_ classes which don't implement a suspension fallback
1092 (everything except svc_acct at the moment). Document better.
1096 sub clone_suspended {
1100 =item clone_kludge_unsuspend
1102 Constructor used by FS::part_export::_export_unsuspend fallback. Stub returning
1103 same object for svc_ classes which don't implement a suspension fallback
1104 (everything except svc_acct at the moment). Document better.
1108 sub clone_kludge_unsuspend {
1112 =item find_duplicates MODE FIELDS...
1114 Method used by _check_duplicate routines to find services with duplicate
1115 values in specified fields. Set MODE to 'global' to search across all
1116 services, or 'export' to limit to those that share one or more exports
1117 with this service. FIELDS is a list of field names; only services
1118 matching in all fields will be returned. Empty fields will be skipped.
1122 sub find_duplicates {
1127 my %search = map { $_ => $self->getfield($_) }
1128 grep { length($self->getfield($_)) } @fields;
1129 return () if !%search;
1130 my @dup = grep { ! $self->svcnum or $_->svcnum != $self->svcnum }
1131 qsearch( $self->table, \%search );
1133 return @dup if $mode eq 'global';
1134 die "incorrect find_duplicates mode '$mode'" if $mode ne 'export';
1136 my $exports = FS::part_export::export_info($self->table);
1137 my %conflict_svcparts;
1138 my $part_svc = $self->part_svc;
1139 foreach my $part_export ( $part_svc->part_export ) {
1140 %conflict_svcparts = map { $_->svcpart => 1 } $part_export->export_svc;
1142 return grep { $conflict_svcparts{$_->cust_svc->svcpart} } @dup;
1145 =item getstatus_html
1149 sub getstatus_html {
1152 my $part_svc = $self->cust_svc->part_svc;
1156 foreach my $export ( grep $_->can('export_getstatus'), $part_svc->part_export ) {
1157 my $export_html = '';
1159 $export->export_getstatus( $self, \$export_html, \%hash );
1160 $html .= $export_html;
1173 my $conf = new FS::Conf;
1174 return '' unless grep { $self->table eq $_ }
1175 $conf->config('nms-auto_add-svc_ips');
1176 my $ip_field = $self->table_info->{'ip_field'};
1178 #XXX perhaps i should be job-queued, i take awhile, right?
1179 my $nms = new FS::NetworkMonitoringSystem;
1180 $nms->add_router( $self->$ip_field(),
1181 $conf->config('nms-auto_add-community')
1190 #XXX not yet implemented
1197 The setfixed method return value.
1199 B<export> method isn't used by insert and replace methods yet.
1203 L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, schema.html
1204 from the base documentation.