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 my $inserted_cust_svc = 0;
248 #unless ( $svcnum ) {
249 if ( !$svcnum or !$cust_svc ) {
250 $cust_svc = new FS::cust_svc ( {
251 #hua?# 'svcnum' => $svcnum,
252 'svcnum' => $self->svcnum,
253 'pkgnum' => $self->pkgnum,
254 'svcpart' => $self->svcpart,
256 my $error = $cust_svc->insert;
258 $dbh->rollback if $oldAutoCommit;
261 $inserted_cust_svc = 1;
262 $svcnum = $self->svcnum($cust_svc->svcnum);
264 #$cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
265 unless ( $cust_svc ) {
266 $dbh->rollback if $oldAutoCommit;
267 return "no cust_svc record found for svcnum ". $self->svcnum;
269 $self->pkgnum($cust_svc->pkgnum);
270 $self->svcpart($cust_svc->svcpart);
273 my $error = $self->preinsert_hook_first
274 || $self->set_auto_inventory
276 || $self->_check_duplicate
277 || $self->preinsert_hook
278 || $self->SUPER::insert;
280 if ( $inserted_cust_svc ) {
281 my $derror = $cust_svc->delete;
282 die $derror if $derror;
284 $dbh->rollback if $oldAutoCommit;
288 foreach my $object ( @$objects ) {
290 if ( ref($object) eq 'ARRAY' ) {
291 ($obj, $field) = @$object;
296 $obj->$field($self->svcnum);
297 $error = $obj->insert;
299 $dbh->rollback if $oldAutoCommit;
305 unless ( $noexport_hack ) {
307 warn "[$me] insert: \$FS::queue::jobnums is $FS::queue::jobnums\n"
310 my $export_args = $options{'export_args'} || [];
312 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
313 my $error = $part_export->export_insert($self, @$export_args);
315 $dbh->rollback if $oldAutoCommit;
316 return "exporting to ". $part_export->exporttype.
317 " (transaction rolled back): $error";
321 foreach my $depend_jobnum ( @$depend_jobnums ) {
322 warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
324 foreach my $jobnum ( @jobnums ) {
325 my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
326 warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
328 my $error = $queue->depend_insert($depend_jobnum);
330 $dbh->rollback if $oldAutoCommit;
331 return "error queuing job dependancy: $error";
338 my $nms_ip_error = $self->nms_ip_insert;
339 if ( $nms_ip_error ) {
340 $dbh->rollback if $oldAutoCommit;
341 return "error queuing IP insert: $nms_ip_error";
344 if ( exists $options{'jobnums'} ) {
345 push @{ $options{'jobnums'} }, @jobnums;
348 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
354 sub preinsert_hook_first { ''; }
355 sub _check_duplcate { ''; }
356 sub preinsert_hook { ''; }
357 sub table_dupcheck_fields { (); }
358 sub predelete_hook { ''; }
359 sub predelete_hook_first { ''; }
361 =item delete [ , OPTION => VALUE ... ]
363 Deletes this account from the database. If there is an error, returns the
364 error, otherwise returns false.
366 The corresponding FS::cust_svc record will be deleted as well.
373 my $export_args = $options{'export_args'} || [];
375 local $SIG{HUP} = 'IGNORE';
376 local $SIG{INT} = 'IGNORE';
377 local $SIG{QUIT} = 'IGNORE';
378 local $SIG{TERM} = 'IGNORE';
379 local $SIG{TSTP} = 'IGNORE';
380 local $SIG{PIPE} = 'IGNORE';
382 my $oldAutoCommit = $FS::UID::AutoCommit;
383 local $FS::UID::AutoCommit = 0;
386 my $error = $self->predelete_hook_first
387 || $self->SUPER::delete
388 || $self->export('delete', @$export_args)
389 || $self->return_inventory
390 || $self->predelete_hook
391 || $self->cust_svc->delete
394 $dbh->rollback if $oldAutoCommit;
398 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
405 Currently this will only run expire exports if any are attached
410 my($self,$date) = (shift,shift);
412 return 'Expire date must be specified' unless $date;
414 local $SIG{HUP} = 'IGNORE';
415 local $SIG{INT} = 'IGNORE';
416 local $SIG{QUIT} = 'IGNORE';
417 local $SIG{TERM} = 'IGNORE';
418 local $SIG{TSTP} = 'IGNORE';
419 local $SIG{PIPE} = 'IGNORE';
421 my $oldAutoCommit = $FS::UID::AutoCommit;
422 local $FS::UID::AutoCommit = 0;
425 my $export_args = [$date];
426 my $error = $self->export('expire', @$export_args);
428 $dbh->rollback if $oldAutoCommit;
432 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
437 =item replace [ OLD_RECORD ] [ HASHREF | OPTION => VALUE ]
439 Replaces OLD_RECORD with this one. If there is an error, returns the error,
440 otherwise returns false.
442 Currently available options are: I<export_args> and I<depend_jobnum>.
444 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
445 jobnums), all provisioning jobs will have a dependancy on the supplied
446 jobnum(s) (they will not run until the specific job(s) complete(s)).
448 If I<export_args> is set to an array reference, the referenced list will be
449 passed to export commands.
456 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
461 ( ref($_[0]) eq 'HASH' )
466 local $FS::queue::jobnums = \@jobnums;
467 warn "[$me] replace: set \$FS::queue::jobnums to $FS::queue::jobnums\n"
469 my $depend_jobnums = $options->{'depend_jobnum'} || [];
470 $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
472 local $SIG{HUP} = 'IGNORE';
473 local $SIG{INT} = 'IGNORE';
474 local $SIG{QUIT} = 'IGNORE';
475 local $SIG{TERM} = 'IGNORE';
476 local $SIG{TSTP} = 'IGNORE';
477 local $SIG{PIPE} = 'IGNORE';
479 my $oldAutoCommit = $FS::UID::AutoCommit;
480 local $FS::UID::AutoCommit = 0;
483 my $error = $new->set_auto_inventory($old);
485 $dbh->rollback if $oldAutoCommit;
489 #redundant, but so any duplicate fields are maniuplated as appropriate
490 # (svc_phone.phonenum)
491 $error = $new->check;
493 $dbh->rollback if $oldAutoCommit;
497 #if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
498 if ( grep { $old->$_ ne $new->$_ } $new->table_dupcheck_fields ) {
500 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
501 $error = $new->_check_duplicate;
503 $dbh->rollback if $oldAutoCommit;
508 $error = $new->SUPER::replace($old);
510 $dbh->rollback if $oldAutoCommit;
515 unless ( $noexport_hack ) {
517 warn "[$me] replace: \$FS::queue::jobnums is $FS::queue::jobnums\n"
520 my $export_args = $options->{'export_args'} || [];
522 #not quite false laziness, but same pattern as FS::svc_acct::replace and
523 #FS::part_export::sqlradius::_export_replace. List::Compare or something
524 #would be useful but too much of a pain in the ass to deploy
526 my @old_part_export = $old->cust_svc->part_svc->part_export;
527 my %old_exportnum = map { $_->exportnum => 1 } @old_part_export;
528 my @new_part_export =
530 ? qsearchs('part_svc', { svcpart=>$new->svcpart } )->part_export
531 : $new->cust_svc->part_svc->part_export;
532 my %new_exportnum = map { $_->exportnum => 1 } @new_part_export;
534 foreach my $delete_part_export (
535 grep { ! $new_exportnum{$_->exportnum} } @old_part_export
537 my $error = $delete_part_export->export_delete($old, @$export_args);
539 $dbh->rollback if $oldAutoCommit;
540 return "error deleting, export to ". $delete_part_export->exporttype.
541 " (transaction rolled back): $error";
545 foreach my $replace_part_export (
546 grep { $old_exportnum{$_->exportnum} } @new_part_export
549 $replace_part_export->export_replace( $new, $old, @$export_args);
551 $dbh->rollback if $oldAutoCommit;
552 return "error exporting to ". $replace_part_export->exporttype.
553 " (transaction rolled back): $error";
557 foreach my $insert_part_export (
558 grep { ! $old_exportnum{$_->exportnum} } @new_part_export
560 my $error = $insert_part_export->export_insert($new, @$export_args );
562 $dbh->rollback if $oldAutoCommit;
563 return "error inserting export to ". $insert_part_export->exporttype.
564 " (transaction rolled back): $error";
568 foreach my $depend_jobnum ( @$depend_jobnums ) {
569 warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
571 foreach my $jobnum ( @jobnums ) {
572 my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
573 warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
575 my $error = $queue->depend_insert($depend_jobnum);
577 $dbh->rollback if $oldAutoCommit;
578 return "error queuing job dependancy: $error";
585 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
591 Sets any fixed fields for this service (see L<FS::part_svc>). If there is an
592 error, returns the error, otherwise returns the FS::part_svc object (use ref()
593 to test the return). Usually called by the check method.
599 $self->setx('F', @_);
604 Sets all fields to their defaults (see L<FS::part_svc>), overriding their
605 current values. If there is an error, returns the error, otherwise returns
606 the FS::part_svc object (use ref() to test the return).
612 $self->setx('D', @_ );
615 =item set_default_and_fixed
619 sub set_default_and_fixed {
621 $self->setx( [ 'D', 'F' ], @_ );
624 =item setx FLAG | FLAG_ARRAYREF , [ CALLBACK_HASHREF ]
626 Sets fields according to the passed in flag or arrayref of flags.
628 Optionally, a hashref of field names and callback coderefs can be passed.
629 If a coderef exists for a given field name, instead of setting the field,
630 the coderef is called with the column value (part_svc_column.columnvalue)
631 as the single parameter.
638 my @x = ref($x) ? @$x : ($x);
639 my $coderef = scalar(@_) ? shift : $self->_fieldhandlers;
642 $self->ut_numbern('svcnum')
644 return $error if $error;
646 my $part_svc = $self->part_svc;
647 return "Unknown svcpart" unless $part_svc;
649 #set default/fixed/whatever fields from part_svc
651 foreach my $part_svc_column (
652 grep { my $f = $_->columnflag; grep { $f eq $_ } @x } #columnflag in @x
653 $part_svc->all_part_svc_column
656 my $columnname = $part_svc_column->columnname;
657 my $columnvalue = $part_svc_column->columnvalue;
659 $columnvalue = &{ $coderef->{$columnname} }( $self, $columnvalue )
660 if exists( $coderef->{$columnname} );
661 $self->setfield( $columnname, $columnvalue );
674 if ( $self->get('svcpart') ) {
675 $svcpart = $self->get('svcpart');
676 } elsif ( $self->svcnum && qsearchs('cust_svc', {'svcnum'=>$self->svcnum}) ) {
677 my $cust_svc = $self->cust_svc;
678 return "Unknown svcnum" unless $cust_svc;
679 $svcpart = $cust_svc->svcpart;
682 qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
688 Returns the FS::svc_pbx record for this service, if any (see L<FS::svc_pbx>).
690 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
695 # XXX FS::h_svc_{acct,phone} could have a history-aware svc_pbx override
699 return '' unless $self->pbxsvc;
700 qsearchs( 'svc_pbx', { 'svcnum' => $self->pbxsvc } );
705 Returns the title of the FS::svc_pbx record associated with this service, if
708 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
715 my $svc_pbx = $self->svc_pbx or return '';
719 =item pbx_select_hash %OPTIONS
721 Can be called as an object method or a class method.
723 Returns a hash SVCNUM => TITLE ... representing the PBXes this customer
724 that may be associated with this service.
726 Currently available options are: I<pkgnum> I<svcpart>
728 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
733 #false laziness w/svc_acct::domain_select_hash
734 sub pbx_select_hash {
735 my ($self, %options) = @_;
741 $part_svc = $self->part_svc;
742 $cust_pkg = $self->cust_svc->cust_pkg
746 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
747 if $options{'svcpart'};
749 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
750 if $options{'pkgnum'};
752 if ($part_svc && ( $part_svc->part_svc_column('pbxsvc')->columnflag eq 'S'
753 || $part_svc->part_svc_column('pbxsvc')->columnflag eq 'F')) {
754 %pbxes = map { $_->svcnum => $_->title }
755 map { qsearchs('svc_pbx', { 'svcnum' => $_ }) }
756 split(',', $part_svc->part_svc_column('pbxsvc')->columnvalue);
757 } elsif ($cust_pkg) { # && !$conf->exists('svc_acct-alldomains') ) {
758 %pbxes = map { $_->svcnum => $_->title }
759 map { qsearchs('svc_pbx', { 'svcnum' => $_->svcnum }) }
760 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
761 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
764 %pbxes = map { $_->svcnum => $_->title } qsearch('svc_pbx', {} );
767 if ($part_svc && $part_svc->part_svc_column('pbxsvc')->columnflag eq 'D') {
768 my $svc_pbx = qsearchs('svc_pbx',
769 { 'svcnum' => $part_svc->part_svc_column('pbxsvc')->columnvalue } );
771 $pbxes{$svc_pbx->svcnum} = $svc_pbx->title;
773 warn "unknown svc_pbx.svcnum for part_svc_column pbxsvc: ".
774 $part_svc->part_svc_column('pbxsvc')->columnvalue;
783 =item set_auto_inventory
785 Sets any fields which auto-populate from inventory (see L<FS::part_svc>), and
786 also check any manually populated inventory fields.
788 If there is an error, returns the error, otherwise returns false.
792 sub set_auto_inventory {
794 my $old = @_ ? shift : '';
797 $self->ut_numbern('svcnum')
799 return $error if $error;
801 my $part_svc = $self->part_svc;
802 return "Unkonwn svcpart" unless $part_svc;
804 local $SIG{HUP} = 'IGNORE';
805 local $SIG{INT} = 'IGNORE';
806 local $SIG{QUIT} = 'IGNORE';
807 local $SIG{TERM} = 'IGNORE';
808 local $SIG{TSTP} = 'IGNORE';
809 local $SIG{PIPE} = 'IGNORE';
811 my $oldAutoCommit = $FS::UID::AutoCommit;
812 local $FS::UID::AutoCommit = 0;
815 #set default/fixed/whatever fields from part_svc
816 my $table = $self->table;
817 foreach my $field ( grep { $_ ne 'svcnum' } $self->fields ) {
819 my $part_svc_column = $part_svc->part_svc_column($field);
820 my $columnflag = $part_svc_column->columnflag;
821 next unless $columnflag =~ /^[AM]$/;
823 next if $columnflag eq 'A' && $self->$field() ne '';
825 my $classnum = $part_svc_column->columnvalue;
826 my %hash = ( 'classnum' => $classnum );
828 if ( $columnflag eq 'A' && $self->$field() eq '' ) {
829 $hash{'svcnum'} = '';
830 } elsif ( $columnflag eq 'M' ) {
831 return "Select inventory item for $field" unless $self->getfield($field);
832 $hash{'item'} = $self->getfield($field);
835 my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql(
837 'table' => 'inventory_item',
840 my $inventory_item = qsearchs({
841 'table' => 'inventory_item',
843 'extra_sql' => "AND $agentnums_sql",
844 'order_by' => 'ORDER BY ( agentnum IS NULL ) '. #agent inventory first
845 ' LIMIT 1 FOR UPDATE',
848 unless ( $inventory_item ) {
849 $dbh->rollback if $oldAutoCommit;
850 my $inventory_class =
851 qsearchs('inventory_class', { 'classnum' => $classnum } );
852 return "Can't find inventory_class.classnum $classnum"
853 unless $inventory_class;
854 return "Out of ". PL_N($inventory_class->classname);
857 next if $columnflag eq 'M' && $inventory_item->svcnum == $self->svcnum;
859 $self->setfield( $field, $inventory_item->item );
860 #if $columnflag eq 'A' && $self->$field() eq '';
862 if ( $old && $old->$field() && $old->$field() ne $self->$field() ) {
863 my $old_inv = qsearchs({
864 'table' => 'inventory_item',
865 'hashref' => { 'classnum' => $classnum,
866 'svcnum' => $old->svcnum,
868 'extra_sql' => ' AND '.
869 '( ( svc_field IS NOT NULL AND svc_field = '.$dbh->quote($field).' )'.
870 ' OR ( svc_field IS NULL AND item = '. dbh->quote($old->$field).' )'.
874 $old_inv->svcnum('');
875 $old_inv->svc_field('');
876 my $oerror = $old_inv->replace;
878 $dbh->rollback if $oldAutoCommit;
879 return "Error unprovisioning inventory: $oerror";
882 warn "old inventory_item not found for $field ". $self->$field;
886 $inventory_item->svcnum( $self->svcnum );
887 $inventory_item->svc_field( $field );
888 my $ierror = $inventory_item->replace();
890 $dbh->rollback if $oldAutoCommit;
891 return "Error provisioning inventory: $ierror";
896 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
902 =item return_inventory
906 sub return_inventory {
909 local $SIG{HUP} = 'IGNORE';
910 local $SIG{INT} = 'IGNORE';
911 local $SIG{QUIT} = 'IGNORE';
912 local $SIG{TERM} = 'IGNORE';
913 local $SIG{TSTP} = 'IGNORE';
914 local $SIG{PIPE} = 'IGNORE';
916 my $oldAutoCommit = $FS::UID::AutoCommit;
917 local $FS::UID::AutoCommit = 0;
920 foreach my $inventory_item ( $self->inventory_item ) {
921 $inventory_item->svcnum('');
922 $inventory_item->svc_field('');
923 my $error = $inventory_item->replace();
925 $dbh->rollback if $oldAutoCommit;
926 return "Error returning inventory: $error";
930 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
937 Returns the inventory items associated with this svc_ record, as
938 FS::inventory_item objects (see L<FS::inventory_item>.
945 'table' => 'inventory_item',
946 'hashref' => { 'svcnum' => $self->svcnum, },
952 Returns the cust_svc record associated with this svc_ record, as a FS::cust_svc
953 object (see L<FS::cust_svc>).
959 qsearchs('cust_svc', { 'svcnum' => $self->svcnum } );
964 Runs export_suspend callbacks.
971 my $export_args = $options{'export_args'} || [];
972 $self->export('suspend', @$export_args);
977 Runs export_unsuspend callbacks.
984 my $export_args = $options{'export_args'} || [];
985 $self->export('unsuspend', @$export_args);
990 Runs export_links callbacks and returns the links.
997 $self->export('links', $return);
1001 =item export_getsettings
1003 Runs export_getsettings callbacks and returns the two hashrefs.
1007 sub export_getsettings {
1011 my $error = $self->export('getsettings', \%settings, \%defaults);
1013 warn "error running export_getsetings: $error";
1014 return ( { 'error' => $error }, {} );
1016 ( \%settings, \%defaults );
1019 =item export_getstatus
1021 Runs export_getstatus callbacks and returns a two item list consisting of an
1022 HTML status and a status hashref.
1026 sub export_getstatus {
1030 my $error = $self->export('getstatus', \$html, \%hash);
1032 warn "error running export_getstatus: $error";
1033 return ( '', { 'error' => $error } );
1038 =item export_setstatus
1040 Runs export_setstatus callbacks. If there is an error, returns the error,
1041 otherwise returns false.
1045 sub export_setstatus { shift->_export_setstatus_X('setstatus', @_) }
1046 sub export_setstatus_listadd { shift->_export_setstatus_X('setstatus_listadd', @_) }
1047 sub export_setstatus_listdel { shift->_export_setstatus_X('setstatus_listdel', @_) }
1048 sub export_setstatus_vacationadd { shift->_export_setstatus_X('setstatus_vacationadd', @_) }
1049 sub export_setstatus_vacationdel { shift->_export_setstatus_X('setstatus_vacationdel', @_) }
1051 sub _export_setstatus_X {
1052 my( $self, $method, @args ) = @_;
1053 my $error = $self->export($method, @args);
1055 warn "error running export_$method: $error";
1061 =item export HOOK [ EXPORT_ARGS ]
1063 Runs the provided export hook (i.e. "suspend", "unsuspend") for this service.
1068 my( $self, $method ) = ( shift, shift );
1070 $method = "export_$method" unless $method =~ /^export_/;
1072 local $SIG{HUP} = 'IGNORE';
1073 local $SIG{INT} = 'IGNORE';
1074 local $SIG{QUIT} = 'IGNORE';
1075 local $SIG{TERM} = 'IGNORE';
1076 local $SIG{TSTP} = 'IGNORE';
1077 local $SIG{PIPE} = 'IGNORE';
1079 my $oldAutoCommit = $FS::UID::AutoCommit;
1080 local $FS::UID::AutoCommit = 0;
1084 unless ( $noexport_hack ) {
1085 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1086 next unless $part_export->can($method);
1087 my $error = $part_export->$method($self, @_);
1089 $dbh->rollback if $oldAutoCommit;
1090 return "error exporting $method event to ". $part_export->exporttype.
1091 " (transaction rolled back): $error";
1096 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1103 Sets or retrieves overlimit date.
1109 #$self->cust_svc->overlimit(@_);
1110 my $cust_svc = $self->cust_svc;
1111 unless ( $cust_svc ) { #wtf?
1112 my $error = "$me overlimit: missing cust_svc record for svc_acct svcnum ".
1114 if ( $overlimit_missing_cust_svc_nonfatal_kludge ) {
1115 cluck "$error; continuing anyway as requested";
1121 $cust_svc->overlimit(@_);
1126 Stub - returns false (no error) so derived classes don't need to define this
1127 methods. Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
1129 This method is called *before* the deletion step which actually deletes the
1130 services. This method should therefore only be used for "pre-deletion"
1131 cancellation steps, if necessary.
1137 =item clone_suspended
1139 Constructor used by FS::part_export::_export_suspend fallback. Stub returning
1140 same object for svc_ classes which don't implement a suspension fallback
1141 (everything except svc_acct at the moment). Document better.
1145 sub clone_suspended {
1149 =item clone_kludge_unsuspend
1151 Constructor used by FS::part_export::_export_unsuspend fallback. Stub returning
1152 same object for svc_ classes which don't implement a suspension fallback
1153 (everything except svc_acct at the moment). Document better.
1157 sub clone_kludge_unsuspend {
1161 =item find_duplicates MODE FIELDS...
1163 Method used by _check_duplicate routines to find services with duplicate
1164 values in specified fields. Set MODE to 'global' to search across all
1165 services, or 'export' to limit to those that share one or more exports
1166 with this service. FIELDS is a list of field names; only services
1167 matching in all fields will be returned. Empty fields will be skipped.
1171 sub find_duplicates {
1176 my %search = map { $_ => $self->getfield($_) }
1177 grep { length($self->getfield($_)) } @fields;
1178 return () if !%search;
1179 my @dup = grep { ! $self->svcnum or $_->svcnum != $self->svcnum }
1180 qsearch( $self->table, \%search );
1182 return @dup if $mode eq 'global';
1183 die "incorrect find_duplicates mode '$mode'" if $mode ne 'export';
1185 my $exports = FS::part_export::export_info($self->table);
1186 my %conflict_svcparts;
1187 my $part_svc = $self->part_svc;
1188 foreach my $part_export ( $part_svc->part_export ) {
1189 %conflict_svcparts = map { $_->svcpart => 1 } $part_export->export_svc;
1191 return grep { $conflict_svcparts{$_->cust_svc->svcpart} } @dup;
1194 =item getstatus_html
1198 sub getstatus_html {
1201 my $part_svc = $self->cust_svc->part_svc;
1205 foreach my $export ( grep $_->can('export_getstatus'), $part_svc->part_export ) {
1206 my $export_html = '';
1208 $export->export_getstatus( $self, \$export_html, \%hash );
1209 $html .= $export_html;
1222 my $conf = new FS::Conf;
1223 return '' unless grep { $self->table eq $_ }
1224 $conf->config('nms-auto_add-svc_ips');
1225 my $ip_field = $self->table_info->{'ip_field'};
1227 my $queue = FS::queue->new( {
1228 'job' => 'FS::NetworkMonitoringSystem::queued_add_router',
1229 'svcnum' => $self->svcnum,
1231 $queue->insert( 'FS::NetworkMonitoringSystem',
1233 $conf->config('nms-auto_add-community')
1242 #XXX not yet implemented
1249 The setfixed method return value.
1251 B<export> method isn't used by insert and replace methods yet.
1255 L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, schema.html
1256 from the base documentation.