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 Only checks fields marked as required in table_info or
159 part_svc_column definition. Should be invoked by service-specific
160 check using SUPER. Invokes FS::Record::check using SUPER.
167 ## Checking required fields
169 # get fields marked as required in table_info
172 my $tinfo = $self->can('table_info') ? $self->table_info : {};
173 if ($tinfo->{'manual_require'}) {
174 my $fields = $tinfo->{'fields'} || {};
175 foreach my $field (keys %$fields) {
176 if (ref($fields->{$field}) && $fields->{$field}->{'required'}) {
177 $required->{$field} = 1;
178 $labels->{$field} = $fields->{$field}->{'label'};
181 # add fields marked as required in database
183 qsearch('part_svc_column',{
184 'svcpart' => $self->svcpart,
188 $required->{$column->columnname} = 1;
189 $labels->{$column->columnname} = $column->columnlabel;
191 # do the actual checking
192 foreach my $field (keys %$required) {
193 unless (length($self->get($field)) > 0) {
194 my $name = $labels->{$field} || $field;
195 return "$name is required\n"
203 =item insert [ , OPTION => VALUE ... ]
205 Adds this record to the database. If there is an error, returns the error,
206 otherwise returns false.
208 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
209 defined. An FS::cust_svc record will be created and inserted.
211 Currently available options are: I<jobnums>, I<child_objects> and
214 If I<jobnum> is set to an array reference, the jobnums of any export jobs will
215 be added to the referenced array.
217 If I<child_objects> is set to an array reference of FS::tablename objects
218 (for example, FS::svc_export_machine or FS::acct_snarf objects), they
219 will have their svcnum field set and will be inserted after this record,
220 but before any exports are run. Each element of the array can also
221 optionally be a two-element array reference containing the child object
222 and the name of an alternate field to be filled in with the newly-inserted
223 svcnum, for example C<[ $svc_forward, 'srcsvc' ]>
225 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
226 jobnums), all provisioning jobs will have a dependancy on the supplied
227 jobnum(s) (they will not run until the specific job(s) complete(s)).
229 If I<export_args> is set to an array reference, the referenced list will be
230 passed to export commands.
237 warn "[$me] insert called with options ".
238 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
242 local $FS::queue::jobnums = \@jobnums;
243 warn "[$me] insert: set \$FS::queue::jobnums to $FS::queue::jobnums\n"
245 my $objects = $options{'child_objects'} || [];
246 my $depend_jobnums = $options{'depend_jobnum'} || [];
247 $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
249 local $SIG{HUP} = 'IGNORE';
250 local $SIG{INT} = 'IGNORE';
251 local $SIG{QUIT} = 'IGNORE';
252 local $SIG{TERM} = 'IGNORE';
253 local $SIG{TSTP} = 'IGNORE';
254 local $SIG{PIPE} = 'IGNORE';
256 my $oldAutoCommit = $FS::UID::AutoCommit;
257 local $FS::UID::AutoCommit = 0;
260 my $svcnum = $self->svcnum;
261 my $cust_svc = $svcnum ? qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) : '';
262 my $inserted_cust_svc = 0;
263 #unless ( $svcnum ) {
264 if ( !$svcnum or !$cust_svc ) {
265 $cust_svc = new FS::cust_svc ( {
266 #hua?# 'svcnum' => $svcnum,
267 'svcnum' => $self->svcnum,
268 'pkgnum' => $self->pkgnum,
269 'svcpart' => $self->svcpart,
271 my $error = $cust_svc->insert;
273 $dbh->rollback if $oldAutoCommit;
276 $inserted_cust_svc = 1;
277 $svcnum = $self->svcnum($cust_svc->svcnum);
279 #$cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
280 unless ( $cust_svc ) {
281 $dbh->rollback if $oldAutoCommit;
282 return "no cust_svc record found for svcnum ". $self->svcnum;
284 $self->pkgnum($cust_svc->pkgnum);
285 $self->svcpart($cust_svc->svcpart);
288 my $error = $self->preinsert_hook_first(%options)
289 || $self->set_auto_inventory
291 || $self->_check_duplicate
292 || $self->preinsert_hook
293 || $self->SUPER::insert;
295 if ( $inserted_cust_svc ) {
296 my $derror = $cust_svc->delete;
297 die $derror if $derror;
299 $dbh->rollback if $oldAutoCommit;
303 foreach my $object ( @$objects ) {
305 if ( ref($object) eq 'ARRAY' ) {
306 ($obj, $field) = @$object;
311 $obj->$field($self->svcnum);
312 $error = $obj->insert;
314 $dbh->rollback if $oldAutoCommit;
320 unless ( $noexport_hack ) {
322 warn "[$me] insert: \$FS::queue::jobnums is $FS::queue::jobnums\n"
325 my $export_args = $options{'export_args'} || [];
327 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
328 my $error = $part_export->export_insert($self, @$export_args);
330 $dbh->rollback if $oldAutoCommit;
331 return "exporting to ". $part_export->exporttype.
332 " (transaction rolled back): $error";
336 foreach my $depend_jobnum ( @$depend_jobnums ) {
337 warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
339 foreach my $jobnum ( @jobnums ) {
340 my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
341 warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
343 my $error = $queue->depend_insert($depend_jobnum);
345 $dbh->rollback if $oldAutoCommit;
346 return "error queuing job dependancy: $error";
353 my $nms_ip_error = $self->nms_ip_insert;
354 if ( $nms_ip_error ) {
355 $dbh->rollback if $oldAutoCommit;
356 return "error queuing IP insert: $nms_ip_error";
359 if ( exists $options{'jobnums'} ) {
360 push @{ $options{'jobnums'} }, @jobnums;
363 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
369 sub preinsert_hook_first { ''; }
370 sub _check_duplcate { ''; }
371 sub preinsert_hook { ''; }
372 sub table_dupcheck_fields { (); }
373 sub prereplace_hook { ''; }
374 sub prereplace_hook_first { ''; }
375 sub predelete_hook { ''; }
376 sub predelete_hook_first { ''; }
378 =item delete [ , OPTION => VALUE ... ]
380 Deletes this account from the database. If there is an error, returns the
381 error, otherwise returns false.
383 The corresponding FS::cust_svc record will be deleted as well.
390 my $export_args = $options{'export_args'} || [];
392 local $SIG{HUP} = 'IGNORE';
393 local $SIG{INT} = 'IGNORE';
394 local $SIG{QUIT} = 'IGNORE';
395 local $SIG{TERM} = 'IGNORE';
396 local $SIG{TSTP} = 'IGNORE';
397 local $SIG{PIPE} = 'IGNORE';
399 my $oldAutoCommit = $FS::UID::AutoCommit;
400 local $FS::UID::AutoCommit = 0;
403 my $error = $self->predelete_hook_first
404 || $self->SUPER::delete
405 || $self->export('delete', @$export_args)
406 || $self->return_inventory
407 || $self->release_router
408 || $self->predelete_hook
409 || $self->cust_svc->delete
412 $dbh->rollback if $oldAutoCommit;
416 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
423 Currently this will only run expire exports if any are attached
428 my($self,$date) = (shift,shift);
430 return 'Expire date must be specified' unless $date;
432 local $SIG{HUP} = 'IGNORE';
433 local $SIG{INT} = 'IGNORE';
434 local $SIG{QUIT} = 'IGNORE';
435 local $SIG{TERM} = 'IGNORE';
436 local $SIG{TSTP} = 'IGNORE';
437 local $SIG{PIPE} = 'IGNORE';
439 my $oldAutoCommit = $FS::UID::AutoCommit;
440 local $FS::UID::AutoCommit = 0;
443 my $export_args = [$date];
444 my $error = $self->export('expire', @$export_args);
446 $dbh->rollback if $oldAutoCommit;
450 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
455 =item replace [ OLD_RECORD ] [ HASHREF | OPTION => VALUE ]
457 Replaces OLD_RECORD with this one. If there is an error, returns the error,
458 otherwise returns false.
460 Currently available options are: I<child_objects>, I<export_args> and
463 If I<child_objects> is set to an array reference of FS::tablename objects
464 (for example, FS::svc_export_machine or FS::acct_snarf objects), they
465 will have their svcnum field set and will be inserted or replaced after
466 this record, but before any exports are run. Each element of the array
467 can also optionally be a two-element array reference containing the
468 child object and the name of an alternate field to be filled in with
469 the newly-inserted svcnum, for example C<[ $svc_forward, 'srcsvc' ]>
471 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
472 jobnums), all provisioning jobs will have a dependancy on the supplied
473 jobnum(s) (they will not run until the specific job(s) complete(s)).
475 If I<export_args> is set to an array reference, the referenced list will be
476 passed to export commands.
483 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
488 ( ref($_[0]) eq 'HASH' )
492 my $objects = $options->{'child_objects'} || [];
495 local $FS::queue::jobnums = \@jobnums;
496 warn "[$me] replace: set \$FS::queue::jobnums to $FS::queue::jobnums\n"
498 my $depend_jobnums = $options->{'depend_jobnum'} || [];
499 $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
501 local $SIG{HUP} = 'IGNORE';
502 local $SIG{INT} = 'IGNORE';
503 local $SIG{QUIT} = 'IGNORE';
504 local $SIG{TERM} = 'IGNORE';
505 local $SIG{TSTP} = 'IGNORE';
506 local $SIG{PIPE} = 'IGNORE';
508 my $oldAutoCommit = $FS::UID::AutoCommit;
509 local $FS::UID::AutoCommit = 0;
512 my $error = $new->prereplace_hook_first($old)
513 || $new->set_auto_inventory($old)
514 || $new->check; #redundant, but so any duplicate fields are
515 #maniuplated as appropriate (svc_phone.phonenum)
517 $dbh->rollback if $oldAutoCommit;
521 #if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
522 if ( grep { $old->$_ ne $new->$_ } $new->table_dupcheck_fields ) {
524 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
525 $error = $new->_check_duplicate;
527 $dbh->rollback if $oldAutoCommit;
532 $error = $new->SUPER::replace($old);
534 $dbh->rollback if $oldAutoCommit;
538 foreach my $object ( @$objects ) {
540 if ( ref($object) eq 'ARRAY' ) {
541 ($obj, $field) = @$object;
546 $obj->$field($new->svcnum);
548 my $oldobj = qsearchs( $obj->table, {
549 $field => $new->svcnum,
550 map { $_ => $obj->$_ } $obj->_svc_child_partfields,
554 my $pkey = $oldobj->primary_key;
555 $obj->$pkey($oldobj->$pkey);
556 $obj->replace($oldobj);
558 $error = $obj->insert;
561 $dbh->rollback if $oldAutoCommit;
567 unless ( $noexport_hack ) {
569 warn "[$me] replace: \$FS::queue::jobnums is $FS::queue::jobnums\n"
572 my $export_args = $options->{'export_args'} || [];
574 #not quite false laziness, but same pattern as FS::svc_acct::replace and
575 #FS::part_export::sqlradius::_export_replace. List::Compare or something
576 #would be useful but too much of a pain in the ass to deploy
578 my @old_part_export = $old->cust_svc->part_svc->part_export;
579 my %old_exportnum = map { $_->exportnum => 1 } @old_part_export;
580 my @new_part_export =
582 ? qsearchs('part_svc', { svcpart=>$new->svcpart } )->part_export
583 : $new->cust_svc->part_svc->part_export;
584 my %new_exportnum = map { $_->exportnum => 1 } @new_part_export;
586 foreach my $delete_part_export (
587 grep { ! $new_exportnum{$_->exportnum} } @old_part_export
589 my $error = $delete_part_export->export_delete($old, @$export_args);
591 $dbh->rollback if $oldAutoCommit;
592 return "error deleting, export to ". $delete_part_export->exporttype.
593 " (transaction rolled back): $error";
597 foreach my $replace_part_export (
598 grep { $old_exportnum{$_->exportnum} } @new_part_export
601 $replace_part_export->export_replace( $new, $old, @$export_args);
603 $dbh->rollback if $oldAutoCommit;
604 return "error exporting to ". $replace_part_export->exporttype.
605 " (transaction rolled back): $error";
609 foreach my $insert_part_export (
610 grep { ! $old_exportnum{$_->exportnum} } @new_part_export
612 my $error = $insert_part_export->export_insert($new, @$export_args );
614 $dbh->rollback if $oldAutoCommit;
615 return "error inserting export to ". $insert_part_export->exporttype.
616 " (transaction rolled back): $error";
620 foreach my $depend_jobnum ( @$depend_jobnums ) {
621 warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
623 foreach my $jobnum ( @jobnums ) {
624 my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
625 warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
627 my $error = $queue->depend_insert($depend_jobnum);
629 $dbh->rollback if $oldAutoCommit;
630 return "error queuing job dependancy: $error";
637 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
643 Sets any fixed fields for this service (see L<FS::part_svc>). If there is an
644 error, returns the error, otherwise returns the FS::part_svc object (use ref()
645 to test the return). Usually called by the check method.
651 $self->setx('F', @_);
656 Sets all fields to their defaults (see L<FS::part_svc>), overriding their
657 current values. If there is an error, returns the error, otherwise returns
658 the FS::part_svc object (use ref() to test the return).
664 $self->setx('D', @_ );
667 =item set_default_and_fixed
671 sub set_default_and_fixed {
673 $self->setx( [ 'D', 'F' ], @_ );
676 =item setx FLAG | FLAG_ARRAYREF , [ CALLBACK_HASHREF ]
678 Sets fields according to the passed in flag or arrayref of flags.
680 Optionally, a hashref of field names and callback coderefs can be passed.
681 If a coderef exists for a given field name, instead of setting the field,
682 the coderef is called with the column value (part_svc_column.columnvalue)
683 as the single parameter.
690 my @x = ref($x) ? @$x : ($x);
691 my $coderef = scalar(@_) ? shift : $self->_fieldhandlers;
694 $self->ut_numbern('svcnum')
696 return $error if $error;
698 my $part_svc = $self->part_svc;
699 return "Unknown svcpart" unless $part_svc;
701 #set default/fixed/whatever fields from part_svc
703 foreach my $part_svc_column (
704 grep { my $f = $_->columnflag; grep { $f eq $_ } @x } #columnflag in @x
705 $part_svc->all_part_svc_column
708 my $columnname = $part_svc_column->columnname;
709 my $columnvalue = $part_svc_column->columnvalue;
711 $columnvalue = &{ $coderef->{$columnname} }( $self, $columnvalue )
712 if exists( $coderef->{$columnname} );
713 $self->setfield( $columnname, $columnvalue );
726 if ( $self->get('svcpart') ) {
727 $svcpart = $self->get('svcpart');
728 } elsif ( $self->svcnum && qsearchs('cust_svc', {'svcnum'=>$self->svcnum}) ) {
729 my $cust_svc = $self->cust_svc;
730 return "Unknown svcnum" unless $cust_svc;
731 $svcpart = $cust_svc->svcpart;
734 qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
740 Returns the FS::svc_pbx record for this service, if any (see L<FS::svc_pbx>).
742 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
747 # XXX FS::h_svc_{acct,phone} could have a history-aware svc_pbx override
751 return '' unless $self->pbxsvc;
752 qsearchs( 'svc_pbx', { 'svcnum' => $self->pbxsvc } );
757 Returns the title of the FS::svc_pbx record associated with this service, if
760 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
767 my $svc_pbx = $self->svc_pbx or return '';
771 =item pbx_select_hash %OPTIONS
773 Can be called as an object method or a class method.
775 Returns a hash SVCNUM => TITLE ... representing the PBXes this customer
776 that may be associated with this service.
778 Currently available options are: I<pkgnum> I<svcpart>
780 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
785 #false laziness w/svc_acct::domain_select_hash
786 sub pbx_select_hash {
787 my ($self, %options) = @_;
793 $part_svc = $self->part_svc;
794 $cust_pkg = $self->cust_svc->cust_pkg
798 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
799 if $options{'svcpart'};
801 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
802 if $options{'pkgnum'};
804 if ($part_svc && ( $part_svc->part_svc_column('pbxsvc')->columnflag eq 'S'
805 || $part_svc->part_svc_column('pbxsvc')->columnflag eq 'F')) {
806 %pbxes = map { $_->svcnum => $_->title }
807 map { qsearchs('svc_pbx', { 'svcnum' => $_ }) }
808 split(',', $part_svc->part_svc_column('pbxsvc')->columnvalue);
809 } elsif ($cust_pkg) { # && !$conf->exists('svc_acct-alldomains') ) {
810 %pbxes = map { $_->svcnum => $_->title }
811 map { qsearchs('svc_pbx', { 'svcnum' => $_->svcnum }) }
812 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
813 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
816 %pbxes = map { $_->svcnum => $_->title } qsearch('svc_pbx', {} );
819 if ($part_svc && $part_svc->part_svc_column('pbxsvc')->columnflag eq 'D') {
820 my $svc_pbx = qsearchs('svc_pbx',
821 { 'svcnum' => $part_svc->part_svc_column('pbxsvc')->columnvalue } );
823 $pbxes{$svc_pbx->svcnum} = $svc_pbx->title;
825 warn "unknown svc_pbx.svcnum for part_svc_column pbxsvc: ".
826 $part_svc->part_svc_column('pbxsvc')->columnvalue;
835 =item set_auto_inventory
837 Sets any fields which auto-populate from inventory (see L<FS::part_svc>), and
838 also check any manually populated inventory fields.
840 If there is an error, returns the error, otherwise returns false.
844 sub set_auto_inventory {
845 # don't try to do this during an upgrade
846 return '' if $FS::CurrentUser::upgrade_hack;
849 my $old = @_ ? shift : '';
852 $self->ut_numbern('svcnum')
854 return $error if $error;
856 my $part_svc = $self->part_svc;
857 return "Unkonwn svcpart" unless $part_svc;
859 local $SIG{HUP} = 'IGNORE';
860 local $SIG{INT} = 'IGNORE';
861 local $SIG{QUIT} = 'IGNORE';
862 local $SIG{TERM} = 'IGNORE';
863 local $SIG{TSTP} = 'IGNORE';
864 local $SIG{PIPE} = 'IGNORE';
866 my $oldAutoCommit = $FS::UID::AutoCommit;
867 local $FS::UID::AutoCommit = 0;
870 #set default/fixed/whatever fields from part_svc
871 my $table = $self->table;
872 foreach my $field ( grep { $_ ne 'svcnum' } $self->fields ) {
874 my $part_svc_column = $part_svc->part_svc_column($field);
875 my $columnflag = $part_svc_column->columnflag;
876 next unless $columnflag =~ /^[AM]$/;
878 next if $columnflag eq 'A' && $self->$field() ne '';
880 my $classnum = $part_svc_column->columnvalue;
883 if ( $columnflag eq 'A' && $self->$field() eq '' ) {
884 $hash{'svcnum'} = '';
885 } elsif ( $columnflag eq 'M' ) {
886 return "Select inventory item for $field" unless $self->getfield($field);
887 $hash{'item'} = $self->getfield($field);
888 my $chosen_classnum = $self->getfield($field.'_classnum');
889 if ( grep {$_ == $chosen_classnum} split(',', $classnum) ) {
890 $classnum = $chosen_classnum;
892 # otherwise the chosen classnum is either (all), or somehow not on
893 # the list, so ignore it and choose the first item that's in any
897 my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql(
899 'table' => 'inventory_item',
902 my $inventory_item = qsearchs({
903 'table' => 'inventory_item',
905 'extra_sql' => "AND classnum IN ($classnum) AND $agentnums_sql",
906 'order_by' => 'ORDER BY ( agentnum IS NULL ) '. #agent inventory first
907 ' LIMIT 1 FOR UPDATE',
910 unless ( $inventory_item ) {
911 # should really only be shown if columnflag eq 'A'...
912 $dbh->rollback if $oldAutoCommit;
913 my $message = 'Out of ';
914 my @classnums = split(',', $classnum);
915 foreach ( @classnums ) {
916 my $class = FS::inventory_class->by_key($_)
917 or return "Can't find inventory_class.classnum $_";
918 $message .= PL_N($class->classname);
919 if ( scalar(@classnums) > 2 ) { # english is hard
920 if ( $_ != $classnums[-1] ) {
924 if ( scalar(@classnums) > 1 and $_ == $classnums[-2] ) {
931 next if $columnflag eq 'M' && $inventory_item->svcnum == $self->svcnum;
933 $self->setfield( $field, $inventory_item->item );
934 #if $columnflag eq 'A' && $self->$field() eq '';
936 # release the old inventory item, if there was one
937 if ( $old && $old->$field() && $old->$field() ne $self->$field() ) {
938 my $old_inv = qsearchs({
939 'table' => 'inventory_item',
941 'svcnum' => $old->svcnum,
943 'extra_sql' => "AND classnum IN ($classnum) AND ".
944 '( ( svc_field IS NOT NULL AND svc_field = '.$dbh->quote($field).' )'.
945 ' OR ( svc_field IS NULL AND item = '. dbh->quote($old->$field).' )'.
949 $old_inv->svcnum('');
950 $old_inv->svc_field('');
951 my $oerror = $old_inv->replace;
953 $dbh->rollback if $oldAutoCommit;
954 return "Error unprovisioning inventory: $oerror";
957 warn "old inventory_item not found for $field ". $self->$field;
961 $inventory_item->svcnum( $self->svcnum );
962 $inventory_item->svc_field( $field );
963 my $ierror = $inventory_item->replace();
965 $dbh->rollback if $oldAutoCommit;
966 return "Error provisioning inventory: $ierror";
971 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
977 =item return_inventory
979 Release all inventory items attached to this service's fields. Call
980 when unprovisioning the service.
984 sub return_inventory {
987 local $SIG{HUP} = 'IGNORE';
988 local $SIG{INT} = 'IGNORE';
989 local $SIG{QUIT} = 'IGNORE';
990 local $SIG{TERM} = 'IGNORE';
991 local $SIG{TSTP} = 'IGNORE';
992 local $SIG{PIPE} = 'IGNORE';
994 my $oldAutoCommit = $FS::UID::AutoCommit;
995 local $FS::UID::AutoCommit = 0;
998 foreach my $inventory_item ( $self->inventory_item ) {
999 $inventory_item->svcnum('');
1000 $inventory_item->svc_field('');
1001 my $error = $inventory_item->replace();
1003 $dbh->rollback if $oldAutoCommit;
1004 return "Error returning inventory: $error";
1008 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1013 =item inventory_item
1015 Returns the inventory items associated with this svc_ record, as
1016 FS::inventory_item objects (see L<FS::inventory_item>.
1020 sub inventory_item {
1023 'table' => 'inventory_item',
1024 'hashref' => { 'svcnum' => $self->svcnum, },
1028 =item release_router
1030 Delete any routers associated with this service. This will release their
1031 address blocks, also.
1035 sub release_router {
1037 my @routers = qsearch('router', { svcnum => $self->svcnum });
1038 foreach (@routers) {
1039 my $error = $_->delete;
1040 return "$error (removing router '".$_->routername."')" if $error;
1048 Returns the cust_svc record associated with this svc_ record, as a FS::cust_svc
1049 object (see L<FS::cust_svc>).
1055 qsearchs('cust_svc', { 'svcnum' => $self->svcnum } );
1060 Runs export_suspend callbacks.
1067 my $export_args = $options{'export_args'} || [];
1068 $self->export('suspend', @$export_args);
1073 Runs export_unsuspend callbacks.
1080 my $export_args = $options{'export_args'} || [];
1081 $self->export('unsuspend', @$export_args);
1086 Runs export_links callbacks and returns the links.
1093 $self->export('links', $return);
1097 =item export_getsettings
1099 Runs export_getsettings callbacks and returns the two hashrefs.
1103 sub export_getsettings {
1107 my $error = $self->export('getsettings', \%settings, \%defaults);
1109 warn "error running export_getsetings: $error";
1110 return ( { 'error' => $error }, {} );
1112 ( \%settings, \%defaults );
1115 =item export_getstatus
1117 Runs export_getstatus callbacks and returns a two item list consisting of an
1118 HTML status and a status hashref.
1122 sub export_getstatus {
1126 my $error = $self->export('getstatus', \$html, \%hash);
1128 warn "error running export_getstatus: $error";
1129 return ( '', { 'error' => $error } );
1134 =item export_setstatus
1136 Runs export_setstatus callbacks. If there is an error, returns the error,
1137 otherwise returns false.
1141 sub export_setstatus { shift->_export_setstatus_X('setstatus', @_) }
1142 sub export_setstatus_listadd { shift->_export_setstatus_X('setstatus_listadd', @_) }
1143 sub export_setstatus_listdel { shift->_export_setstatus_X('setstatus_listdel', @_) }
1144 sub export_setstatus_vacationadd { shift->_export_setstatus_X('setstatus_vacationadd', @_) }
1145 sub export_setstatus_vacationdel { shift->_export_setstatus_X('setstatus_vacationdel', @_) }
1147 sub _export_setstatus_X {
1148 my( $self, $method, @args ) = @_;
1149 my $error = $self->export($method, @args);
1151 warn "error running export_$method: $error";
1157 =item export HOOK [ EXPORT_ARGS ]
1159 Runs the provided export hook (i.e. "suspend", "unsuspend") for this service.
1164 my( $self, $method ) = ( shift, shift );
1166 # $method must start with export_, $action must be the part after that
1167 $method = "export_$method" unless $method =~ /^export_/;
1168 my ($action) = $method =~ /^export_(\w+)/;
1170 local $SIG{HUP} = 'IGNORE';
1171 local $SIG{INT} = 'IGNORE';
1172 local $SIG{QUIT} = 'IGNORE';
1173 local $SIG{TERM} = 'IGNORE';
1174 local $SIG{TSTP} = 'IGNORE';
1175 local $SIG{PIPE} = 'IGNORE';
1177 my $oldAutoCommit = $FS::UID::AutoCommit;
1178 local $FS::UID::AutoCommit = 0;
1182 unless ( $noexport_hack ) {
1183 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1184 next unless $part_export->can($method);
1185 next if $part_export->get("no_$action"); # currently only 'no_suspend'
1186 my $error = $part_export->$method($self, @_);
1188 $dbh->rollback if $oldAutoCommit;
1189 return "error exporting $method event to ". $part_export->exporttype.
1190 " (transaction rolled back): $error";
1195 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1202 Sets or retrieves overlimit date.
1208 #$self->cust_svc->overlimit(@_);
1209 my $cust_svc = $self->cust_svc;
1210 unless ( $cust_svc ) { #wtf?
1211 my $error = "$me overlimit: missing cust_svc record for svc_acct svcnum ".
1213 if ( $overlimit_missing_cust_svc_nonfatal_kludge ) {
1214 cluck "$error; continuing anyway as requested";
1220 $cust_svc->overlimit(@_);
1225 Stub - returns false (no error) so derived classes don't need to define this
1226 methods. Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
1228 This method is called *before* the deletion step which actually deletes the
1229 services. This method should therefore only be used for "pre-deletion"
1230 cancellation steps, if necessary.
1236 =item clone_suspended
1238 Constructor used by FS::part_export::_export_suspend fallback. Stub returning
1239 same object for svc_ classes which don't implement a suspension fallback
1240 (everything except svc_acct at the moment). Document better.
1244 sub clone_suspended {
1248 =item clone_kludge_unsuspend
1250 Constructor used by FS::part_export::_export_unsuspend fallback. Stub returning
1251 same object for svc_ classes which don't implement a suspension fallback
1252 (everything except svc_acct at the moment). Document better.
1256 sub clone_kludge_unsuspend {
1260 =item find_duplicates MODE FIELDS...
1262 Method used by _check_duplicate routines to find services with duplicate
1263 values in specified fields. Set MODE to 'global' to search across all
1264 services, or 'export' to limit to those that share one or more exports
1265 with this service. FIELDS is a list of field names; only services
1266 matching in all fields will be returned. Empty fields will be skipped.
1270 sub find_duplicates {
1275 my %search = map { $_ => $self->getfield($_) }
1276 grep { length($self->getfield($_)) } @fields;
1277 return () if !%search;
1278 my @dup = grep { ! $self->svcnum or $_->svcnum != $self->svcnum }
1279 qsearch( $self->table, \%search );
1281 return @dup if $mode eq 'global';
1282 die "incorrect find_duplicates mode '$mode'" if $mode ne 'export';
1284 my $exports = FS::part_export::export_info($self->table);
1285 my %conflict_svcparts;
1286 my $part_svc = $self->part_svc;
1287 foreach my $part_export ( $part_svc->part_export ) {
1288 %conflict_svcparts = map { $_->svcpart => 1 } $part_export->export_svc;
1290 return grep { $conflict_svcparts{$_->cust_svc->svcpart} } @dup;
1293 =item getstatus_html
1297 sub getstatus_html {
1300 my $part_svc = $self->cust_svc->part_svc;
1304 foreach my $export ( grep $_->can('export_getstatus'), $part_svc->part_export ) {
1305 my $export_html = '';
1307 $export->export_getstatus( $self, \$export_html, \%hash );
1308 $html .= $export_html;
1321 my $conf = new FS::Conf;
1322 return '' unless grep { $self->table eq $_ }
1323 $conf->config('nms-auto_add-svc_ips');
1324 my $ip_field = $self->table_info->{'ip_field'};
1326 my $queue = FS::queue->new( {
1327 'job' => 'FS::NetworkMonitoringSystem::queued_add_router',
1328 'svcnum' => $self->svcnum,
1330 $queue->insert( 'FS::NetworkMonitoringSystem',
1332 $conf->config('nms-auto_add-community')
1341 #XXX not yet implemented
1344 =item search_sql_field FIELD STRING
1346 Class method which returns an SQL fragment to search for STRING in FIELD.
1348 It is now case-insensitive by default.
1352 sub search_sql_field {
1353 my( $class, $field, $string ) = @_;
1354 my $table = $class->table;
1355 my $q_string = dbh->quote($string);
1356 "LOWER($table.$field) = LOWER($q_string)";
1359 #fallback for services that don't provide a search...
1361 #my( $class, $string ) = @_;
1364 sub search_sql_addl_from {
1368 =item search HASHREF
1370 Class method which returns a qsearch hash expression to search for parameters
1371 specified in HASHREF.
1377 =item unlinked - set to search for all unlinked services. Overrides all other options.
1387 =item pkgpart - arrayref
1389 =item routernum - arrayref
1391 =item sectornum - arrayref
1393 =item towernum - arrayref
1397 =item cancelled - if true, only returns svcs attached to cancelled pkgs;
1398 if defined and false, only returns svcs not attached to cancelled packages
1404 ### Don't call the 'cancelled' option 'Service Status'
1405 ### There is no such thing
1406 ### See cautionary note in httemplate/browse/part_svc.cgi
1409 my ($class, $params) = @_;
1412 'LEFT JOIN cust_svc USING ( svcnum )',
1413 'LEFT JOIN part_svc USING ( svcpart )',
1414 'LEFT JOIN cust_pkg USING ( pkgnum )',
1415 FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'),
1420 $class->_search_svc($params, \@from, \@where) if $class->can('_search_svc');
1423 # if ( $params->{'domain'} ) {
1424 # my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
1425 # #preserve previous behavior & bubble up an error if $svc_domain not found?
1426 # push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
1430 # if ( $params->{'domsvc'} =~ /^(\d+)$/ ) {
1431 # push @where, "domsvc = $1";
1435 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
1438 if ( $params->{'agentnum'} =~ /^(\d+)$/ && $1 ) {
1439 push @where, "cust_main.agentnum = $1";
1443 if ( $params->{'custnum'} =~ /^(\d+)$/ && $1 ) {
1444 push @where, "cust_pkg.custnum = $1";
1448 if ( $params->{'cust_status'} =~ /^([a-z]+)$/ ) {
1449 push @where, FS::cust_main->cust_status_sql . " = '$1'";
1453 if ( $params->{'balance'} =~ /^\s*(\-?\d*(\.\d{1,2})?)\s*$/ && length($1) ) {
1457 if ( $params->{'balance_days'} =~ /^\s*(\d*(\.\d{1,3})?)\s*$/ && length($1) ) {
1458 $age = time - 86400 * $1;
1460 push @where, FS::cust_main->balance_date_sql($age) . " > $balance";
1464 if ( $params->{'payby'} && scalar(@{ $params->{'payby'} }) ) {
1465 my @payby = map "'$_'", grep /^(\w+)$/, @{ $params->{'payby'} };
1466 push @where, 'payby IN ('. join(',', @payby ). ')';
1470 ##pkgpart, now properly untainted, can be arrayref
1471 #for my $pkgpart ( $params->{'pkgpart'} ) {
1472 # if ( ref $pkgpart ) {
1473 # my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
1474 # push @where, "cust_pkg.pkgpart IN ($where)" if $where;
1476 # elsif ( $pkgpart =~ /^(\d+)$/ ) {
1477 # push @where, "cust_pkg.pkgpart = $1";
1480 if ( $params->{'pkgpart'} ) {
1481 my @pkgpart = ref( $params->{'pkgpart'} )
1482 ? @{ $params->{'pkgpart'} }
1483 : $params->{'pkgpart'}
1484 ? ( $params->{'pkgpart'} )
1486 @pkgpart = grep /^(\d+)$/, @pkgpart;
1487 push @where, 'cust_pkg.pkgpart IN ('. join(',', @pkgpart ). ')' if @pkgpart;
1491 if ( $params->{'svcnum'} ) {
1492 my @svcnum = ref( $params->{'svcnum'} )
1493 ? @{ $params->{'svcnum'} }
1494 : $params->{'svcnum'};
1495 @svcnum = grep /^\d+$/, @svcnum;
1496 push @where, 'svcnum IN ('. join(',', @svcnum) . ')' if @svcnum;
1500 if ( $params->{'svcpart'} ) {
1501 my @svcpart = ref( $params->{'svcpart'} )
1502 ? @{ $params->{'svcpart'} }
1503 : $params->{'svcpart'}
1504 ? ( $params->{'svcpart'} )
1506 @svcpart = grep /^(\d+)$/, @svcpart;
1507 push @where, 'svcpart IN ('. join(',', @svcpart ). ')' if @svcpart;
1510 if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
1511 push @from, ' LEFT JOIN export_svc USING ( svcpart )';
1512 push @where, "exportnum = $1";
1515 if ( defined($params->{'cancelled'}) ) {
1516 if ($params->{'cancelled'}) {
1517 push @where, "cust_pkg.cancel IS NOT NULL";
1519 push @where, "cust_pkg.cancel IS NULL";
1523 # # sector and tower
1524 # my @where_sector = $class->tower_sector_sql($params);
1525 # if ( @where_sector ) {
1526 # push @where, @where_sector;
1527 # push @from, ' LEFT JOIN tower_sector USING ( sectornum )';
1530 # here is the agent virtualization
1531 #if ($params->{CurrentUser}) {
1533 # qsearchs('access_user', { username => $params->{CurrentUser} });
1535 # if ($access_user) {
1536 # push @where, $access_user->agentnums_sql('table'=>'cust_main');
1538 # push @where, "1=0";
1541 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
1542 'table' => 'cust_main',
1543 'null_right' => 'View/link unlinked services',
1547 push @where, @{ $params->{'where'} } if $params->{'where'};
1549 my $addl_from = join(' ', @from);
1550 my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
1552 my $table = $class->table;
1554 my $count_query = "SELECT COUNT(*) FROM $table $addl_from $extra_sql";
1555 #if ( keys %svc_X ) {
1556 # $count_query .= ' WHERE '.
1557 # join(' AND ', map "$_ = ". dbh->quote($svc_X{$_}),
1565 'select' => join(', ',
1568 'cust_main.custnum',
1569 @{ $params->{'addl_select'} || [] },
1570 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
1572 'addl_from' => $addl_from,
1573 'extra_sql' => $extra_sql,
1574 'order_by' => $params->{'order_by'},
1575 'count_query' => $count_query,
1584 The setfixed method return value.
1586 B<export> method isn't used by insert and replace methods yet.
1590 L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, schema.html
1591 from the base documentation.