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.
52 my $class = ref($proto) || $proto;
54 bless ($self, $class);
56 unless ( defined ( $self->table ) ) {
57 $self->{'Table'} = shift;
58 carp "warning: FS::Record::new called with table name ". $self->{'Table'};
61 #$self->{'Hash'} = shift;
63 $self->{'Hash'} = { map { $_ => $newhash->{$_} } qw(svcnum svcpart) };
65 $self->setdefault( $self->_fieldhandlers )
68 $self->{'Hash'}{$_} = $newhash->{$_}
69 foreach grep { defined($newhash->{$_}) && length($newhash->{$_}) }
72 foreach my $field ( grep !defined($self->{'Hash'}{$_}), $self->fields ) {
73 $self->{'Hash'}{$field}='';
76 $self->_rebless if $self->can('_rebless');
78 $self->{'modified'} = 0;
80 $self->_cache($self->{'Hash'}, shift) if $self->can('_cache') && @_;
86 sub _fieldhandlers { {}; }
90 # This restricts the fields based on part_svc_column and the svcpart of
91 # the service. There are four possible cases:
92 # 1. svcpart passed as part of the svc_x hash.
93 # 2. svcpart fetched via cust_svc based on svcnum.
94 # 3. No svcnum or svcpart. In this case, return ALL the fields with
95 # dbtable eq $self->table.
96 # 4. Called via "fields('svc_acct')" or something similar. In this case
97 # there is no $self object.
101 my @vfields = $self->SUPER::virtual_fields;
103 return @vfields unless (ref $self); # Case 4
105 if ($self->svcpart) { # Case 1
106 $svcpart = $self->svcpart;
107 } elsif ( $self->svcnum
108 && qsearchs('cust_svc',{'svcnum'=>$self->svcnum} )
110 $svcpart = $self->cust_svc->svcpart;
115 if ($svcpart) { #Cases 1 and 2
116 my %flags = map { $_->columnname, $_->columnflag } (
117 qsearch ('part_svc_column', { svcpart => $svcpart } )
119 return grep { not ( defined($flags{$_}) && $flags{$_} eq 'X') } @vfields;
128 svc_Common provides a fallback label subroutine that just returns the svcnum.
134 cluck "warning: ". ref($self). " not loaded or missing label method; ".
146 (($self->cust_svc || return)->cust_pkg || return)->cust_main || return
151 defined($self->cust_main);
156 Checks the validity of fields in this record.
158 At present, this does nothing but call FS::Record::check (which, in turn,
159 does nothing but run virtual field checks).
168 =item insert [ , OPTION => VALUE ... ]
170 Adds this record to the database. If there is an error, returns the error,
171 otherwise returns false.
173 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
174 defined. An FS::cust_svc record will be created and inserted.
176 Currently available options are: I<jobnums>, I<child_objects> and
179 If I<jobnum> is set to an array reference, the jobnums of any export jobs will
180 be added to the referenced array.
182 If I<child_objects> is set to an array reference of FS::tablename objects (for
183 example, FS::acct_snarf objects), they will have their svcnum field set and
184 will be inserted after this record, but before any exports are run. Each
185 element of the array can also optionally be a two-element array reference
186 containing the child object and the name of an alternate field to be filled in
187 with the newly-inserted svcnum, for example C<[ $svc_forward, 'srcsvc' ]>
189 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
190 jobnums), all provisioning jobs will have a dependancy on the supplied
191 jobnum(s) (they will not run until the specific job(s) complete(s)).
193 If I<export_args> is set to an array reference, the referenced list will be
194 passed to export commands.
201 warn "[$me] insert called with options ".
202 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
206 local $FS::queue::jobnums = \@jobnums;
207 warn "[$me] insert: set \$FS::queue::jobnums to $FS::queue::jobnums\n"
209 my $objects = $options{'child_objects'} || [];
210 my $depend_jobnums = $options{'depend_jobnum'} || [];
211 $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
213 local $SIG{HUP} = 'IGNORE';
214 local $SIG{INT} = 'IGNORE';
215 local $SIG{QUIT} = 'IGNORE';
216 local $SIG{TERM} = 'IGNORE';
217 local $SIG{TSTP} = 'IGNORE';
218 local $SIG{PIPE} = 'IGNORE';
220 my $oldAutoCommit = $FS::UID::AutoCommit;
221 local $FS::UID::AutoCommit = 0;
224 my $svcnum = $self->svcnum;
225 my $cust_svc = $svcnum ? qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) : '';
226 my $inserted_cust_svc = 0;
227 #unless ( $svcnum ) {
228 if ( !$svcnum or !$cust_svc ) {
229 $cust_svc = new FS::cust_svc ( {
230 #hua?# 'svcnum' => $svcnum,
231 'svcnum' => $self->svcnum,
232 'pkgnum' => $self->pkgnum,
233 'svcpart' => $self->svcpart,
235 my $error = $cust_svc->insert;
237 $dbh->rollback if $oldAutoCommit;
240 $inserted_cust_svc = 1;
241 $svcnum = $self->svcnum($cust_svc->svcnum);
243 #$cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
244 unless ( $cust_svc ) {
245 $dbh->rollback if $oldAutoCommit;
246 return "no cust_svc record found for svcnum ". $self->svcnum;
248 $self->pkgnum($cust_svc->pkgnum);
249 $self->svcpart($cust_svc->svcpart);
252 my $error = $self->preinsert_hook_first
253 || $self->set_auto_inventory
255 || $self->_check_duplicate
256 || $self->preinsert_hook
257 || $self->SUPER::insert;
259 if ( $inserted_cust_svc ) {
260 my $derror = $cust_svc->delete;
261 die $derror if $derror;
263 $dbh->rollback if $oldAutoCommit;
267 foreach my $object ( @$objects ) {
269 if ( ref($object) eq 'ARRAY' ) {
270 ($obj, $field) = @$object;
275 $obj->$field($self->svcnum);
276 $error = $obj->insert;
278 $dbh->rollback if $oldAutoCommit;
284 unless ( $noexport_hack ) {
286 warn "[$me] insert: \$FS::queue::jobnums is $FS::queue::jobnums\n"
289 my $export_args = $options{'export_args'} || [];
291 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
292 my $error = $part_export->export_insert($self, @$export_args);
294 $dbh->rollback if $oldAutoCommit;
295 return "exporting to ". $part_export->exporttype.
296 " (transaction rolled back): $error";
300 foreach my $depend_jobnum ( @$depend_jobnums ) {
301 warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
303 foreach my $jobnum ( @jobnums ) {
304 my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
305 warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
307 my $error = $queue->depend_insert($depend_jobnum);
309 $dbh->rollback if $oldAutoCommit;
310 return "error queuing job dependancy: $error";
317 my $nms_ip_error = $self->nms_ip_insert;
318 if ( $nms_ip_error ) {
319 $dbh->rollback if $oldAutoCommit;
320 return "error queuing IP insert: $nms_ip_error";
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 ". PL_N($inventory_class->classname);
836 next if $columnflag eq 'M' && $inventory_item->svcnum == $self->svcnum;
838 $self->setfield( $field, $inventory_item->item );
839 #if $columnflag eq 'A' && $self->$field() eq '';
841 if ( $old && $old->$field() && $old->$field() ne $self->$field() ) {
842 my $old_inv = qsearchs({
843 'table' => 'inventory_item',
844 'hashref' => { 'classnum' => $classnum,
845 'svcnum' => $old->svcnum,
847 'extra_sql' => ' AND '.
848 '( ( svc_field IS NOT NULL AND svc_field = '.$dbh->quote($field).' )'.
849 ' OR ( svc_field IS NULL AND item = '. dbh->quote($old->$field).' )'.
853 $old_inv->svcnum('');
854 $old_inv->svc_field('');
855 my $oerror = $old_inv->replace;
857 $dbh->rollback if $oldAutoCommit;
858 return "Error unprovisioning inventory: $oerror";
861 warn "old inventory_item not found for $field ". $self->$field;
865 $inventory_item->svcnum( $self->svcnum );
866 $inventory_item->svc_field( $field );
867 my $ierror = $inventory_item->replace();
869 $dbh->rollback if $oldAutoCommit;
870 return "Error provisioning inventory: $ierror";
875 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
881 =item return_inventory
885 sub return_inventory {
888 local $SIG{HUP} = 'IGNORE';
889 local $SIG{INT} = 'IGNORE';
890 local $SIG{QUIT} = 'IGNORE';
891 local $SIG{TERM} = 'IGNORE';
892 local $SIG{TSTP} = 'IGNORE';
893 local $SIG{PIPE} = 'IGNORE';
895 my $oldAutoCommit = $FS::UID::AutoCommit;
896 local $FS::UID::AutoCommit = 0;
899 foreach my $inventory_item ( $self->inventory_item ) {
900 $inventory_item->svcnum('');
901 $inventory_item->svc_field('');
902 my $error = $inventory_item->replace();
904 $dbh->rollback if $oldAutoCommit;
905 return "Error returning inventory: $error";
909 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
916 Returns the inventory items associated with this svc_ record, as
917 FS::inventory_item objects (see L<FS::inventory_item>.
924 'table' => 'inventory_item',
925 'hashref' => { 'svcnum' => $self->svcnum, },
931 Returns the cust_svc record associated with this svc_ record, as a FS::cust_svc
932 object (see L<FS::cust_svc>).
938 qsearchs('cust_svc', { 'svcnum' => $self->svcnum } );
943 Runs export_suspend callbacks.
950 my $export_args = $options{'export_args'} || [];
951 $self->export('suspend', @$export_args);
956 Runs export_unsuspend callbacks.
963 my $export_args = $options{'export_args'} || [];
964 $self->export('unsuspend', @$export_args);
969 Runs export_links callbacks and returns the links.
976 $self->export('links', $return);
980 =item export_getsettings
982 Runs export_getsettings callbacks and returns the two hashrefs.
986 sub export_getsettings {
990 my $error = $self->export('getsettings', \%settings, \%defaults);
992 warn "error running export_getsetings: $error";
993 return ( { 'error' => $error }, {} );
995 ( \%settings, \%defaults );
998 =item export_getstatus
1000 Runs export_getstatus callbacks and returns a two item list consisting of an
1001 HTML status and a status hashref.
1005 sub export_getstatus {
1009 my $error = $self->export('getstatus', \$html, \%hash);
1011 warn "error running export_getstatus: $error";
1012 return ( '', { 'error' => $error } );
1017 =item export_setstatus
1019 Runs export_setstatus callbacks. If there is an error, returns the error,
1020 otherwise returns false.
1024 sub export_setstatus { shift->_export_setstatus_X('setstatus', @_) }
1025 sub export_setstatus_listadd { shift->_export_setstatus_X('setstatus_listadd', @_) }
1026 sub export_setstatus_listdel { shift->_export_setstatus_X('setstatus_listdel', @_) }
1027 sub export_setstatus_vacationadd { shift->_export_setstatus_X('setstatus_vacationadd', @_) }
1028 sub export_setstatus_vacationdel { shift->_export_setstatus_X('setstatus_vacationdel', @_) }
1030 sub _export_setstatus_X {
1031 my( $self, $method, @args ) = @_;
1032 my $error = $self->export($method, @args);
1034 warn "error running export_$method: $error";
1040 =item export HOOK [ EXPORT_ARGS ]
1042 Runs the provided export hook (i.e. "suspend", "unsuspend") for this service.
1047 my( $self, $method ) = ( shift, shift );
1049 $method = "export_$method" unless $method =~ /^export_/;
1051 local $SIG{HUP} = 'IGNORE';
1052 local $SIG{INT} = 'IGNORE';
1053 local $SIG{QUIT} = 'IGNORE';
1054 local $SIG{TERM} = 'IGNORE';
1055 local $SIG{TSTP} = 'IGNORE';
1056 local $SIG{PIPE} = 'IGNORE';
1058 my $oldAutoCommit = $FS::UID::AutoCommit;
1059 local $FS::UID::AutoCommit = 0;
1063 unless ( $noexport_hack ) {
1064 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1065 next unless $part_export->can($method);
1066 my $error = $part_export->$method($self, @_);
1068 $dbh->rollback if $oldAutoCommit;
1069 return "error exporting $method event to ". $part_export->exporttype.
1070 " (transaction rolled back): $error";
1075 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1082 Sets or retrieves overlimit date.
1088 #$self->cust_svc->overlimit(@_);
1089 my $cust_svc = $self->cust_svc;
1090 unless ( $cust_svc ) { #wtf?
1091 my $error = "$me overlimit: missing cust_svc record for svc_acct svcnum ".
1093 if ( $overlimit_missing_cust_svc_nonfatal_kludge ) {
1094 cluck "$error; continuing anyway as requested";
1100 $cust_svc->overlimit(@_);
1105 Stub - returns false (no error) so derived classes don't need to define this
1106 methods. Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
1108 This method is called *before* the deletion step which actually deletes the
1109 services. This method should therefore only be used for "pre-deletion"
1110 cancellation steps, if necessary.
1116 =item clone_suspended
1118 Constructor used by FS::part_export::_export_suspend fallback. Stub returning
1119 same object for svc_ classes which don't implement a suspension fallback
1120 (everything except svc_acct at the moment). Document better.
1124 sub clone_suspended {
1128 =item clone_kludge_unsuspend
1130 Constructor used by FS::part_export::_export_unsuspend fallback. Stub returning
1131 same object for svc_ classes which don't implement a suspension fallback
1132 (everything except svc_acct at the moment). Document better.
1136 sub clone_kludge_unsuspend {
1140 =item find_duplicates MODE FIELDS...
1142 Method used by _check_duplicate routines to find services with duplicate
1143 values in specified fields. Set MODE to 'global' to search across all
1144 services, or 'export' to limit to those that share one or more exports
1145 with this service. FIELDS is a list of field names; only services
1146 matching in all fields will be returned. Empty fields will be skipped.
1150 sub find_duplicates {
1155 my %search = map { $_ => $self->getfield($_) }
1156 grep { length($self->getfield($_)) } @fields;
1157 return () if !%search;
1158 my @dup = grep { ! $self->svcnum or $_->svcnum != $self->svcnum }
1159 qsearch( $self->table, \%search );
1161 return @dup if $mode eq 'global';
1162 die "incorrect find_duplicates mode '$mode'" if $mode ne 'export';
1164 my $exports = FS::part_export::export_info($self->table);
1165 my %conflict_svcparts;
1166 my $part_svc = $self->part_svc;
1167 foreach my $part_export ( $part_svc->part_export ) {
1168 %conflict_svcparts = map { $_->svcpart => 1 } $part_export->export_svc;
1170 return grep { $conflict_svcparts{$_->cust_svc->svcpart} } @dup;
1173 =item getstatus_html
1177 sub getstatus_html {
1180 my $part_svc = $self->cust_svc->part_svc;
1184 foreach my $export ( grep $_->can('export_getstatus'), $part_svc->part_export ) {
1185 my $export_html = '';
1187 $export->export_getstatus( $self, \$export_html, \%hash );
1188 $html .= $export_html;
1201 my $conf = new FS::Conf;
1202 return '' unless grep { $self->table eq $_ }
1203 $conf->config('nms-auto_add-svc_ips');
1204 my $ip_field = $self->table_info->{'ip_field'};
1206 my $queue = FS::queue->new( {
1207 'job' => 'FS::NetworkMonitoringSystem::queued_add_router',
1208 'svcnum' => $self->svcnum,
1210 $queue->insert( 'FS::NetworkMonitoringSystem',
1212 $conf->config('nms-auto_add-community')
1221 #XXX not yet implemented
1224 =item search_sql_field FIELD STRING
1226 Class method which returns an SQL fragment to search for STRING in FIELD.
1228 It is now case-insensitive by default.
1232 sub search_sql_field {
1233 my( $class, $field, $string ) = @_;
1234 my $table = $class->table;
1235 my $q_string = dbh->quote($string);
1236 "LOWER($table.$field) = LOWER($q_string)";
1239 #fallback for services that don't provide a search...
1241 #my( $class, $string ) = @_;
1245 =item search HASHREF
1247 Class method which returns a qsearch hash expression to search for parameters
1248 specified in HASHREF.
1254 =item unlinked - set to search for all unlinked services. Overrides all other options.
1264 =item pkgpart - arrayref
1266 =item routernum - arrayref
1268 =item sectornum - arrayref
1270 =item towernum - arrayref
1278 # based on FS::svc_acct::search, both that and svc_broadband::search should
1279 # eventually use this instead
1281 my ($class, $params) = @_;
1284 'LEFT JOIN cust_svc USING ( svcnum )',
1285 'LEFT JOIN part_svc USING ( svcpart )',
1286 'LEFT JOIN cust_pkg USING ( pkgnum )',
1287 'LEFT JOIN cust_main USING ( custnum )',
1293 # if ( $params->{'domain'} ) {
1294 # my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
1295 # #preserve previous behavior & bubble up an error if $svc_domain not found?
1296 # push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
1300 # if ( $params->{'domsvc'} =~ /^(\d+)$/ ) {
1301 # push @where, "domsvc = $1";
1305 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
1308 if ( $params->{'agentnum'} =~ /^(\d+)$/ && $1 ) {
1309 push @where, "cust_main.agentnum = $1";
1313 if ( $params->{'custnum'} =~ /^(\d+)$/ && $1 ) {
1314 push @where, "custnum = $1";
1318 if ( $params->{'cust_status'} =~ /^([a-z]+)$/ ) {
1319 push @where, FS::cust_main->cust_status_sql . " = '$1'";
1323 if ( $params->{'balance'} =~ /^\s*(\-?\d*(\.\d{1,2})?)\s*$/ && length($1) ) {
1327 if ( $params->{'balance_days'} =~ /^\s*(\d*(\.\d{1,3})?)\s*$/ && length($1) ) {
1328 $age = time - 86400 * $1;
1330 push @where, FS::cust_main->balance_date_sql($age) . " > $balance";
1334 if ( $params->{'payby'} && scalar(@{ $params->{'payby'} }) ) {
1335 my @payby = map "'$_'", grep /^(\w+)$/, @{ $params->{'payby'} };
1336 push @where, 'payby IN ('. join(',', @payby ). ')';
1340 if ( $params->{'pkgpart'} && scalar(@{ $params->{'pkgpart'} }) ) {
1341 my @pkgpart = grep /^(\d+)$/, @{ $params->{'pkgpart'} };
1342 push @where, 'cust_pkg.pkgpart IN ('. join(',', @pkgpart ). ')';
1346 if ( $params->{'svcpart'} && scalar(@{ $params->{'svcpart'} }) ) {
1347 my @svcpart = grep /^(\d+)$/, @{ $params->{'svcpart'} };
1348 push @where, 'svcpart IN ('. join(',', @svcpart ). ')';
1351 if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
1352 push @from, ' LEFT JOIN export_svc USING ( svcpart )';
1353 push @where, "exportnum = $1";
1356 # # sector and tower
1357 # my @where_sector = $class->tower_sector_sql($params);
1358 # if ( @where_sector ) {
1359 # push @where, @where_sector;
1360 # push @from, ' LEFT JOIN tower_sector USING ( sectornum )';
1363 # here is the agent virtualization
1364 #if ($params->{CurrentUser}) {
1366 # qsearchs('access_user', { username => $params->{CurrentUser} });
1368 # if ($access_user) {
1369 # push @where, $access_user->agentnums_sql('table'=>'cust_main');
1371 # push @where, "1=0";
1374 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
1375 'table' => 'cust_main',
1376 'null_right' => 'View/link unlinked services',
1380 push @where, @{ $params->{'where'} } if $params->{'where'};
1382 my $addl_from = join(' ', @from);
1383 my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
1385 my $table = $class->table;
1387 my $count_query = "SELECT COUNT(*) FROM $table $addl_from $extra_sql";
1388 #if ( keys %svc_X ) {
1389 # $count_query .= ' WHERE '.
1390 # join(' AND ', map "$_ = ". dbh->quote($svc_X{$_}),
1398 'select' => join(', ',
1401 'cust_main.custnum',
1402 @{ $params->{'addl_select'} || [] },
1403 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
1405 'addl_from' => $addl_from,
1406 'extra_sql' => $extra_sql,
1407 'order_by' => $params->{'order_by'},
1408 'count_query' => $count_query,
1417 The setfixed method return value.
1419 B<export> method isn't used by insert and replace methods yet.
1423 L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, schema.html
1424 from the base documentation.