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
204 (for example, FS::svc_export_machine or FS::acct_snarf objects), they
205 will have their svcnum field set and will be inserted after this record,
206 but before any exports are run. Each element of the array can also
207 optionally be a two-element array reference containing the child object
208 and the name of an alternate field to be filled in with the newly-inserted
209 svcnum, for example C<[ $svc_forward, 'srcsvc' ]>
211 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
212 jobnums), all provisioning jobs will have a dependancy on the supplied
213 jobnum(s) (they will not run until the specific job(s) complete(s)).
215 If I<export_args> is set to an array reference, the referenced list will be
216 passed to export commands.
223 warn "[$me] insert called with options ".
224 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
228 local $FS::queue::jobnums = \@jobnums;
229 warn "[$me] insert: set \$FS::queue::jobnums to $FS::queue::jobnums\n"
231 my $objects = $options{'child_objects'} || [];
232 my $depend_jobnums = $options{'depend_jobnum'} || [];
233 $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
235 local $SIG{HUP} = 'IGNORE';
236 local $SIG{INT} = 'IGNORE';
237 local $SIG{QUIT} = 'IGNORE';
238 local $SIG{TERM} = 'IGNORE';
239 local $SIG{TSTP} = 'IGNORE';
240 local $SIG{PIPE} = 'IGNORE';
242 my $oldAutoCommit = $FS::UID::AutoCommit;
243 local $FS::UID::AutoCommit = 0;
246 my $svcnum = $self->svcnum;
247 my $cust_svc = $svcnum ? qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) : '';
248 my $inserted_cust_svc = 0;
249 #unless ( $svcnum ) {
250 if ( !$svcnum or !$cust_svc ) {
251 $cust_svc = new FS::cust_svc ( {
252 #hua?# 'svcnum' => $svcnum,
253 'svcnum' => $self->svcnum,
254 'pkgnum' => $self->pkgnum,
255 'svcpart' => $self->svcpart,
257 my $error = $cust_svc->insert;
259 $dbh->rollback if $oldAutoCommit;
262 $inserted_cust_svc = 1;
263 $svcnum = $self->svcnum($cust_svc->svcnum);
265 #$cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
266 unless ( $cust_svc ) {
267 $dbh->rollback if $oldAutoCommit;
268 return "no cust_svc record found for svcnum ". $self->svcnum;
270 $self->pkgnum($cust_svc->pkgnum);
271 $self->svcpart($cust_svc->svcpart);
274 my $error = $self->preinsert_hook_first
275 || $self->set_auto_inventory
277 || $self->_check_duplicate
278 || $self->preinsert_hook
279 || $self->SUPER::insert;
281 if ( $inserted_cust_svc ) {
282 my $derror = $cust_svc->delete;
283 die $derror if $derror;
285 $dbh->rollback if $oldAutoCommit;
289 foreach my $object ( @$objects ) {
291 if ( ref($object) eq 'ARRAY' ) {
292 ($obj, $field) = @$object;
297 $obj->$field($self->svcnum);
298 $error = $obj->insert;
300 $dbh->rollback if $oldAutoCommit;
306 unless ( $noexport_hack ) {
308 warn "[$me] insert: \$FS::queue::jobnums is $FS::queue::jobnums\n"
311 my $export_args = $options{'export_args'} || [];
313 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
314 my $error = $part_export->export_insert($self, @$export_args);
316 $dbh->rollback if $oldAutoCommit;
317 return "exporting to ". $part_export->exporttype.
318 " (transaction rolled back): $error";
322 foreach my $depend_jobnum ( @$depend_jobnums ) {
323 warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
325 foreach my $jobnum ( @jobnums ) {
326 my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
327 warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
329 my $error = $queue->depend_insert($depend_jobnum);
331 $dbh->rollback if $oldAutoCommit;
332 return "error queuing job dependancy: $error";
339 my $nms_ip_error = $self->nms_ip_insert;
340 if ( $nms_ip_error ) {
341 $dbh->rollback if $oldAutoCommit;
342 return "error queuing IP insert: $nms_ip_error";
345 if ( exists $options{'jobnums'} ) {
346 push @{ $options{'jobnums'} }, @jobnums;
349 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
355 sub preinsert_hook_first { ''; }
356 sub _check_duplcate { ''; }
357 sub preinsert_hook { ''; }
358 sub table_dupcheck_fields { (); }
359 sub predelete_hook { ''; }
360 sub predelete_hook_first { ''; }
362 =item delete [ , OPTION => VALUE ... ]
364 Deletes this account from the database. If there is an error, returns the
365 error, otherwise returns false.
367 The corresponding FS::cust_svc record will be deleted as well.
374 my $export_args = $options{'export_args'} || [];
376 local $SIG{HUP} = 'IGNORE';
377 local $SIG{INT} = 'IGNORE';
378 local $SIG{QUIT} = 'IGNORE';
379 local $SIG{TERM} = 'IGNORE';
380 local $SIG{TSTP} = 'IGNORE';
381 local $SIG{PIPE} = 'IGNORE';
383 my $oldAutoCommit = $FS::UID::AutoCommit;
384 local $FS::UID::AutoCommit = 0;
387 my $error = $self->predelete_hook_first
388 || $self->SUPER::delete
389 || $self->export('delete', @$export_args)
390 || $self->return_inventory
391 || $self->predelete_hook
392 || $self->cust_svc->delete
395 $dbh->rollback if $oldAutoCommit;
399 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
406 Currently this will only run expire exports if any are attached
411 my($self,$date) = (shift,shift);
413 return 'Expire date must be specified' unless $date;
415 local $SIG{HUP} = 'IGNORE';
416 local $SIG{INT} = 'IGNORE';
417 local $SIG{QUIT} = 'IGNORE';
418 local $SIG{TERM} = 'IGNORE';
419 local $SIG{TSTP} = 'IGNORE';
420 local $SIG{PIPE} = 'IGNORE';
422 my $oldAutoCommit = $FS::UID::AutoCommit;
423 local $FS::UID::AutoCommit = 0;
426 my $export_args = [$date];
427 my $error = $self->export('expire', @$export_args);
429 $dbh->rollback if $oldAutoCommit;
433 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
438 =item replace [ OLD_RECORD ] [ HASHREF | OPTION => VALUE ]
440 Replaces OLD_RECORD with this one. If there is an error, returns the error,
441 otherwise returns false.
443 Currently available options are: I<child_objects>, I<export_args> and
446 If I<child_objects> is set to an array reference of FS::tablename objects
447 (for example, FS::svc_export_machine or FS::acct_snarf objects), they
448 will have their svcnum field set and will be inserted or replaced after
449 this record, but before any exports are run. Each element of the array
450 can also optionally be a two-element array reference containing the
451 child object and the name of an alternate field to be filled in with
452 the newly-inserted svcnum, for example C<[ $svc_forward, 'srcsvc' ]>
454 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
455 jobnums), all provisioning jobs will have a dependancy on the supplied
456 jobnum(s) (they will not run until the specific job(s) complete(s)).
458 If I<export_args> is set to an array reference, the referenced list will be
459 passed to export commands.
466 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
471 ( ref($_[0]) eq 'HASH' )
475 my $objects = $options->{'child_objects'} || [];
478 local $FS::queue::jobnums = \@jobnums;
479 warn "[$me] replace: set \$FS::queue::jobnums to $FS::queue::jobnums\n"
481 my $depend_jobnums = $options->{'depend_jobnum'} || [];
482 $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
484 local $SIG{HUP} = 'IGNORE';
485 local $SIG{INT} = 'IGNORE';
486 local $SIG{QUIT} = 'IGNORE';
487 local $SIG{TERM} = 'IGNORE';
488 local $SIG{TSTP} = 'IGNORE';
489 local $SIG{PIPE} = 'IGNORE';
491 my $oldAutoCommit = $FS::UID::AutoCommit;
492 local $FS::UID::AutoCommit = 0;
495 my $error = $new->set_auto_inventory($old);
497 $dbh->rollback if $oldAutoCommit;
501 #redundant, but so any duplicate fields are maniuplated as appropriate
502 # (svc_phone.phonenum)
503 $error = $new->check;
505 $dbh->rollback if $oldAutoCommit;
509 #if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
510 if ( grep { $old->$_ ne $new->$_ } $new->table_dupcheck_fields ) {
512 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
513 $error = $new->_check_duplicate;
515 $dbh->rollback if $oldAutoCommit;
520 $error = $new->SUPER::replace($old);
522 $dbh->rollback if $oldAutoCommit;
526 foreach my $object ( @$objects ) {
528 if ( ref($object) eq 'ARRAY' ) {
529 ($obj, $field) = @$object;
534 $obj->$field($new->svcnum);
536 my $oldobj = qsearchs( $obj->table, {
537 $field => $new->svcnum,
538 map { $_ => $obj->$_ } $obj->_svc_child_partfields,
542 my $pkey = $oldobj->primary_key;
543 $obj->$pkey($oldobj->$pkey);
544 $obj->replace($oldobj);
546 $error = $obj->insert;
549 $dbh->rollback if $oldAutoCommit;
555 unless ( $noexport_hack ) {
557 warn "[$me] replace: \$FS::queue::jobnums is $FS::queue::jobnums\n"
560 my $export_args = $options->{'export_args'} || [];
562 #not quite false laziness, but same pattern as FS::svc_acct::replace and
563 #FS::part_export::sqlradius::_export_replace. List::Compare or something
564 #would be useful but too much of a pain in the ass to deploy
566 my @old_part_export = $old->cust_svc->part_svc->part_export;
567 my %old_exportnum = map { $_->exportnum => 1 } @old_part_export;
568 my @new_part_export =
570 ? qsearchs('part_svc', { svcpart=>$new->svcpart } )->part_export
571 : $new->cust_svc->part_svc->part_export;
572 my %new_exportnum = map { $_->exportnum => 1 } @new_part_export;
574 foreach my $delete_part_export (
575 grep { ! $new_exportnum{$_->exportnum} } @old_part_export
577 my $error = $delete_part_export->export_delete($old, @$export_args);
579 $dbh->rollback if $oldAutoCommit;
580 return "error deleting, export to ". $delete_part_export->exporttype.
581 " (transaction rolled back): $error";
585 foreach my $replace_part_export (
586 grep { $old_exportnum{$_->exportnum} } @new_part_export
589 $replace_part_export->export_replace( $new, $old, @$export_args);
591 $dbh->rollback if $oldAutoCommit;
592 return "error exporting to ". $replace_part_export->exporttype.
593 " (transaction rolled back): $error";
597 foreach my $insert_part_export (
598 grep { ! $old_exportnum{$_->exportnum} } @new_part_export
600 my $error = $insert_part_export->export_insert($new, @$export_args );
602 $dbh->rollback if $oldAutoCommit;
603 return "error inserting export to ". $insert_part_export->exporttype.
604 " (transaction rolled back): $error";
608 foreach my $depend_jobnum ( @$depend_jobnums ) {
609 warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
611 foreach my $jobnum ( @jobnums ) {
612 my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
613 warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
615 my $error = $queue->depend_insert($depend_jobnum);
617 $dbh->rollback if $oldAutoCommit;
618 return "error queuing job dependancy: $error";
625 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
631 Sets any fixed fields for this service (see L<FS::part_svc>). If there is an
632 error, returns the error, otherwise returns the FS::part_svc object (use ref()
633 to test the return). Usually called by the check method.
639 $self->setx('F', @_);
644 Sets all fields to their defaults (see L<FS::part_svc>), overriding their
645 current values. If there is an error, returns the error, otherwise returns
646 the FS::part_svc object (use ref() to test the return).
652 $self->setx('D', @_ );
655 =item set_default_and_fixed
659 sub set_default_and_fixed {
661 $self->setx( [ 'D', 'F' ], @_ );
664 =item setx FLAG | FLAG_ARRAYREF , [ CALLBACK_HASHREF ]
666 Sets fields according to the passed in flag or arrayref of flags.
668 Optionally, a hashref of field names and callback coderefs can be passed.
669 If a coderef exists for a given field name, instead of setting the field,
670 the coderef is called with the column value (part_svc_column.columnvalue)
671 as the single parameter.
678 my @x = ref($x) ? @$x : ($x);
679 my $coderef = scalar(@_) ? shift : $self->_fieldhandlers;
682 $self->ut_numbern('svcnum')
684 return $error if $error;
686 my $part_svc = $self->part_svc;
687 return "Unknown svcpart" unless $part_svc;
689 #set default/fixed/whatever fields from part_svc
691 foreach my $part_svc_column (
692 grep { my $f = $_->columnflag; grep { $f eq $_ } @x } #columnflag in @x
693 $part_svc->all_part_svc_column
696 my $columnname = $part_svc_column->columnname;
697 my $columnvalue = $part_svc_column->columnvalue;
699 $columnvalue = &{ $coderef->{$columnname} }( $self, $columnvalue )
700 if exists( $coderef->{$columnname} );
701 $self->setfield( $columnname, $columnvalue );
714 if ( $self->get('svcpart') ) {
715 $svcpart = $self->get('svcpart');
716 } elsif ( $self->svcnum && qsearchs('cust_svc', {'svcnum'=>$self->svcnum}) ) {
717 my $cust_svc = $self->cust_svc;
718 return "Unknown svcnum" unless $cust_svc;
719 $svcpart = $cust_svc->svcpart;
722 qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
728 Returns the FS::svc_pbx record for this service, if any (see L<FS::svc_pbx>).
730 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
735 # XXX FS::h_svc_{acct,phone} could have a history-aware svc_pbx override
739 return '' unless $self->pbxsvc;
740 qsearchs( 'svc_pbx', { 'svcnum' => $self->pbxsvc } );
745 Returns the title of the FS::svc_pbx record associated with this service, if
748 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
755 my $svc_pbx = $self->svc_pbx or return '';
759 =item pbx_select_hash %OPTIONS
761 Can be called as an object method or a class method.
763 Returns a hash SVCNUM => TITLE ... representing the PBXes this customer
764 that may be associated with this service.
766 Currently available options are: I<pkgnum> I<svcpart>
768 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
773 #false laziness w/svc_acct::domain_select_hash
774 sub pbx_select_hash {
775 my ($self, %options) = @_;
781 $part_svc = $self->part_svc;
782 $cust_pkg = $self->cust_svc->cust_pkg
786 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
787 if $options{'svcpart'};
789 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
790 if $options{'pkgnum'};
792 if ($part_svc && ( $part_svc->part_svc_column('pbxsvc')->columnflag eq 'S'
793 || $part_svc->part_svc_column('pbxsvc')->columnflag eq 'F')) {
794 %pbxes = map { $_->svcnum => $_->title }
795 map { qsearchs('svc_pbx', { 'svcnum' => $_ }) }
796 split(',', $part_svc->part_svc_column('pbxsvc')->columnvalue);
797 } elsif ($cust_pkg) { # && !$conf->exists('svc_acct-alldomains') ) {
798 %pbxes = map { $_->svcnum => $_->title }
799 map { qsearchs('svc_pbx', { 'svcnum' => $_->svcnum }) }
800 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
801 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
804 %pbxes = map { $_->svcnum => $_->title } qsearch('svc_pbx', {} );
807 if ($part_svc && $part_svc->part_svc_column('pbxsvc')->columnflag eq 'D') {
808 my $svc_pbx = qsearchs('svc_pbx',
809 { 'svcnum' => $part_svc->part_svc_column('pbxsvc')->columnvalue } );
811 $pbxes{$svc_pbx->svcnum} = $svc_pbx->title;
813 warn "unknown svc_pbx.svcnum for part_svc_column pbxsvc: ".
814 $part_svc->part_svc_column('pbxsvc')->columnvalue;
823 =item set_auto_inventory
825 Sets any fields which auto-populate from inventory (see L<FS::part_svc>), and
826 also check any manually populated inventory fields.
828 If there is an error, returns the error, otherwise returns false.
832 sub set_auto_inventory {
834 my $old = @_ ? shift : '';
837 $self->ut_numbern('svcnum')
839 return $error if $error;
841 my $part_svc = $self->part_svc;
842 return "Unkonwn svcpart" unless $part_svc;
844 local $SIG{HUP} = 'IGNORE';
845 local $SIG{INT} = 'IGNORE';
846 local $SIG{QUIT} = 'IGNORE';
847 local $SIG{TERM} = 'IGNORE';
848 local $SIG{TSTP} = 'IGNORE';
849 local $SIG{PIPE} = 'IGNORE';
851 my $oldAutoCommit = $FS::UID::AutoCommit;
852 local $FS::UID::AutoCommit = 0;
855 #set default/fixed/whatever fields from part_svc
856 my $table = $self->table;
857 foreach my $field ( grep { $_ ne 'svcnum' } $self->fields ) {
859 my $part_svc_column = $part_svc->part_svc_column($field);
860 my $columnflag = $part_svc_column->columnflag;
861 next unless $columnflag =~ /^[AM]$/;
863 next if $columnflag eq 'A' && $self->$field() ne '';
865 my $classnum = $part_svc_column->columnvalue;
866 my %hash = ( 'classnum' => $classnum );
868 if ( $columnflag eq 'A' && $self->$field() eq '' ) {
869 $hash{'svcnum'} = '';
870 } elsif ( $columnflag eq 'M' ) {
871 return "Select inventory item for $field" unless $self->getfield($field);
872 $hash{'item'} = $self->getfield($field);
875 my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql(
877 'table' => 'inventory_item',
880 my $inventory_item = qsearchs({
881 'table' => 'inventory_item',
883 'extra_sql' => "AND $agentnums_sql",
884 'order_by' => 'ORDER BY ( agentnum IS NULL ) '. #agent inventory first
885 ' LIMIT 1 FOR UPDATE',
888 unless ( $inventory_item ) {
889 $dbh->rollback if $oldAutoCommit;
890 my $inventory_class =
891 qsearchs('inventory_class', { 'classnum' => $classnum } );
892 return "Can't find inventory_class.classnum $classnum"
893 unless $inventory_class;
894 return "Out of ". PL_N($inventory_class->classname);
897 next if $columnflag eq 'M' && $inventory_item->svcnum == $self->svcnum;
899 $self->setfield( $field, $inventory_item->item );
900 #if $columnflag eq 'A' && $self->$field() eq '';
902 if ( $old && $old->$field() && $old->$field() ne $self->$field() ) {
903 my $old_inv = qsearchs({
904 'table' => 'inventory_item',
905 'hashref' => { 'classnum' => $classnum,
906 'svcnum' => $old->svcnum,
908 'extra_sql' => ' AND '.
909 '( ( svc_field IS NOT NULL AND svc_field = '.$dbh->quote($field).' )'.
910 ' OR ( svc_field IS NULL AND item = '. dbh->quote($old->$field).' )'.
914 $old_inv->svcnum('');
915 $old_inv->svc_field('');
916 my $oerror = $old_inv->replace;
918 $dbh->rollback if $oldAutoCommit;
919 return "Error unprovisioning inventory: $oerror";
922 warn "old inventory_item not found for $field ". $self->$field;
926 $inventory_item->svcnum( $self->svcnum );
927 $inventory_item->svc_field( $field );
928 my $ierror = $inventory_item->replace();
930 $dbh->rollback if $oldAutoCommit;
931 return "Error provisioning inventory: $ierror";
936 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
942 =item return_inventory
946 sub return_inventory {
949 local $SIG{HUP} = 'IGNORE';
950 local $SIG{INT} = 'IGNORE';
951 local $SIG{QUIT} = 'IGNORE';
952 local $SIG{TERM} = 'IGNORE';
953 local $SIG{TSTP} = 'IGNORE';
954 local $SIG{PIPE} = 'IGNORE';
956 my $oldAutoCommit = $FS::UID::AutoCommit;
957 local $FS::UID::AutoCommit = 0;
960 foreach my $inventory_item ( $self->inventory_item ) {
961 $inventory_item->svcnum('');
962 $inventory_item->svc_field('');
963 my $error = $inventory_item->replace();
965 $dbh->rollback if $oldAutoCommit;
966 return "Error returning inventory: $error";
970 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
977 Returns the inventory items associated with this svc_ record, as
978 FS::inventory_item objects (see L<FS::inventory_item>.
985 'table' => 'inventory_item',
986 'hashref' => { 'svcnum' => $self->svcnum, },
992 Returns the cust_svc record associated with this svc_ record, as a FS::cust_svc
993 object (see L<FS::cust_svc>).
999 qsearchs('cust_svc', { 'svcnum' => $self->svcnum } );
1004 Runs export_suspend callbacks.
1011 my $export_args = $options{'export_args'} || [];
1012 $self->export('suspend', @$export_args);
1017 Runs export_unsuspend callbacks.
1024 my $export_args = $options{'export_args'} || [];
1025 $self->export('unsuspend', @$export_args);
1030 Runs export_links callbacks and returns the links.
1037 $self->export('links', $return);
1041 =item export_getsettings
1043 Runs export_getsettings callbacks and returns the two hashrefs.
1047 sub export_getsettings {
1051 my $error = $self->export('getsettings', \%settings, \%defaults);
1053 warn "error running export_getsetings: $error";
1054 return ( { 'error' => $error }, {} );
1056 ( \%settings, \%defaults );
1059 =item export_getstatus
1061 Runs export_getstatus callbacks and returns a two item list consisting of an
1062 HTML status and a status hashref.
1066 sub export_getstatus {
1070 my $error = $self->export('getstatus', \$html, \%hash);
1072 warn "error running export_getstatus: $error";
1073 return ( '', { 'error' => $error } );
1078 =item export_setstatus
1080 Runs export_setstatus callbacks. If there is an error, returns the error,
1081 otherwise returns false.
1085 sub export_setstatus {
1086 my( $self, @args ) = @_;
1087 my $error = $self->export('setstatus', @args);
1089 warn "error running export_setstatus: $error";
1095 sub export_setstatus_listadd {
1096 my( $self, @args ) = @_;
1097 my $error = $self->export('setstatus_listadd', @args);
1099 warn "error running export_setstatus: $error";
1105 sub export_setstatus_listdel {
1106 my( $self, @args ) = @_;
1107 my $error = $self->export('setstatus_listdel', @args);
1109 warn "error running export_setstatus: $error";
1115 =item export HOOK [ EXPORT_ARGS ]
1117 Runs the provided export hook (i.e. "suspend", "unsuspend") for this service.
1122 my( $self, $method ) = ( shift, shift );
1124 $method = "export_$method" unless $method =~ /^export_/;
1126 local $SIG{HUP} = 'IGNORE';
1127 local $SIG{INT} = 'IGNORE';
1128 local $SIG{QUIT} = 'IGNORE';
1129 local $SIG{TERM} = 'IGNORE';
1130 local $SIG{TSTP} = 'IGNORE';
1131 local $SIG{PIPE} = 'IGNORE';
1133 my $oldAutoCommit = $FS::UID::AutoCommit;
1134 local $FS::UID::AutoCommit = 0;
1138 unless ( $noexport_hack ) {
1139 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1140 next unless $part_export->can($method);
1141 my $error = $part_export->$method($self, @_);
1143 $dbh->rollback if $oldAutoCommit;
1144 return "error exporting $method event to ". $part_export->exporttype.
1145 " (transaction rolled back): $error";
1150 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1157 Sets or retrieves overlimit date.
1163 #$self->cust_svc->overlimit(@_);
1164 my $cust_svc = $self->cust_svc;
1165 unless ( $cust_svc ) { #wtf?
1166 my $error = "$me overlimit: missing cust_svc record for svc_acct svcnum ".
1168 if ( $overlimit_missing_cust_svc_nonfatal_kludge ) {
1169 cluck "$error; continuing anyway as requested";
1175 $cust_svc->overlimit(@_);
1180 Stub - returns false (no error) so derived classes don't need to define this
1181 methods. Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
1183 This method is called *before* the deletion step which actually deletes the
1184 services. This method should therefore only be used for "pre-deletion"
1185 cancellation steps, if necessary.
1191 =item clone_suspended
1193 Constructor used by FS::part_export::_export_suspend fallback. Stub returning
1194 same object for svc_ classes which don't implement a suspension fallback
1195 (everything except svc_acct at the moment). Document better.
1199 sub clone_suspended {
1203 =item clone_kludge_unsuspend
1205 Constructor used by FS::part_export::_export_unsuspend fallback. Stub returning
1206 same object for svc_ classes which don't implement a suspension fallback
1207 (everything except svc_acct at the moment). Document better.
1211 sub clone_kludge_unsuspend {
1215 =item find_duplicates MODE FIELDS...
1217 Method used by _check_duplicate routines to find services with duplicate
1218 values in specified fields. Set MODE to 'global' to search across all
1219 services, or 'export' to limit to those that share one or more exports
1220 with this service. FIELDS is a list of field names; only services
1221 matching in all fields will be returned. Empty fields will be skipped.
1225 sub find_duplicates {
1230 my %search = map { $_ => $self->getfield($_) }
1231 grep { length($self->getfield($_)) } @fields;
1232 return () if !%search;
1233 my @dup = grep { ! $self->svcnum or $_->svcnum != $self->svcnum }
1234 qsearch( $self->table, \%search );
1236 return @dup if $mode eq 'global';
1237 die "incorrect find_duplicates mode '$mode'" if $mode ne 'export';
1239 my $exports = FS::part_export::export_info($self->table);
1240 my %conflict_svcparts;
1241 my $part_svc = $self->part_svc;
1242 foreach my $part_export ( $part_svc->part_export ) {
1243 %conflict_svcparts = map { $_->svcpart => 1 } $part_export->export_svc;
1245 return grep { $conflict_svcparts{$_->cust_svc->svcpart} } @dup;
1248 =item getstatus_html
1252 sub getstatus_html {
1255 my $part_svc = $self->cust_svc->part_svc;
1259 foreach my $export ( grep $_->can('export_getstatus'), $part_svc->part_export ) {
1260 my $export_html = '';
1262 $export->export_getstatus( $self, \$export_html, \%hash );
1263 $html .= $export_html;
1276 my $conf = new FS::Conf;
1277 return '' unless grep { $self->table eq $_ }
1278 $conf->config('nms-auto_add-svc_ips');
1279 my $ip_field = $self->table_info->{'ip_field'};
1281 my $queue = FS::queue->new( {
1282 'job' => 'FS::NetworkMonitoringSystem::queued_add_router',
1283 'svcnum' => $self->svcnum,
1285 $queue->insert( 'FS::NetworkMonitoringSystem',
1287 $conf->config('nms-auto_add-community')
1296 #XXX not yet implemented
1303 The setfixed method return value.
1305 B<export> method isn't used by insert and replace methods yet.
1309 L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, schema.html
1310 from the base documentation.