1 package FS::svc_Common;
2 use base qw( FS::cust_main_Mixin FS::Record );
5 use vars qw( $noexport_hack $DEBUG $me
6 $overlimit_missing_cust_svc_nonfatal_kludge );
7 use Carp qw( cluck carp croak confess ); #specify cluck have to specify them all
8 use Scalar::Util qw( blessed );
9 use Lingua::EN::Inflect qw( PL_N );
11 use FS::Record qw( qsearch qsearchs fields dbh );
16 use FS::inventory_item;
17 use FS::inventory_class;
18 use FS::NetworkMonitoringSystem;
20 $me = '[FS::svc_Common]';
23 $overlimit_missing_cust_svc_nonfatal_kludge = 0;
27 FS::svc_Common - Object method for all svc_ records
31 package svc_myservice;
32 use base qw( FS::svc_Common );
36 FS::svc_Common is intended as a base class for table-specific classes to
37 inherit from, i.e. FS::svc_acct. FS::svc_Common inherits from FS::Record.
49 my $class = ref($proto) || $proto;
51 bless ($self, $class);
53 unless ( defined ( $self->table ) ) {
54 $self->{'Table'} = shift;
55 carp "warning: FS::Record::new called with table name ". $self->{'Table'};
58 #$self->{'Hash'} = shift;
60 $self->{'Hash'} = { map { $_ => $newhash->{$_} } qw(svcnum svcpart) };
62 $self->setdefault( $self->_fieldhandlers )
65 $self->{'Hash'}{$_} = $newhash->{$_}
66 foreach grep { defined($newhash->{$_}) && length($newhash->{$_}) }
69 foreach my $field ( grep !defined($self->{'Hash'}{$_}), $self->fields ) {
70 $self->{'Hash'}{$field}='';
73 $self->_rebless if $self->can('_rebless');
75 $self->{'modified'} = 0;
77 $self->_cache($self->{'Hash'}, shift) if $self->can('_cache') && @_;
83 sub _fieldhandlers { {}; }
87 # This restricts the fields based on part_svc_column and the svcpart of
88 # the service. There are four possible cases:
89 # 1. svcpart passed as part of the svc_x hash.
90 # 2. svcpart fetched via cust_svc based on svcnum.
91 # 3. No svcnum or svcpart. In this case, return ALL the fields with
92 # dbtable eq $self->table.
93 # 4. Called via "fields('svc_acct')" or something similar. In this case
94 # there is no $self object.
98 my @vfields = $self->SUPER::virtual_fields;
100 return @vfields unless (ref $self); # Case 4
102 if ($self->svcpart) { # Case 1
103 $svcpart = $self->svcpart;
104 } elsif ( $self->svcnum
105 && qsearchs('cust_svc',{'svcnum'=>$self->svcnum} )
107 $svcpart = $self->cust_svc->svcpart;
112 if ($svcpart) { #Cases 1 and 2
113 my %flags = map { $_->columnname, $_->columnflag } (
114 qsearch ('part_svc_column', { svcpart => $svcpart } )
116 return grep { not ( defined($flags{$_}) && $flags{$_} eq 'X') } @vfields;
125 svc_Common provides a fallback label subroutine that just returns the svcnum.
131 cluck "warning: ". ref($self). " not loaded or missing label method; ".
143 (($self->cust_svc || return)->cust_pkg || return)->cust_main || return
148 defined($self->cust_main);
153 Checks the validity of fields in this record.
155 Only checks fields marked as required in table_info or
156 part_svc_column definition. Should be invoked by service-specific
157 check using SUPER. Invokes FS::Record::check using SUPER.
164 ## Checking required fields
166 # get fields marked as required in table_info
169 my $tinfo = $self->can('table_info') ? $self->table_info : {};
170 my $fields = $tinfo->{'fields'} || {};
171 foreach my $field (keys %$fields) {
172 if (ref($fields->{$field}) && $fields->{$field}->{'required'}) {
173 $required->{$field} = 1;
174 $labels->{$field} = $fields->{$field}->{'label'};
177 # add fields marked as required in database
179 qsearch('part_svc_column',{
180 'svcpart' => $self->svcpart,
184 $required->{$column->columnname} = 1;
185 $labels->{$column->columnname} = $column->columnlabel;
187 # do the actual checking
188 foreach my $field (keys %$required) {
189 unless ($self->$field) {
190 my $name = $labels->{$field} || $field;
191 return "Field $name is required\n"
198 =item insert [ , OPTION => VALUE ... ]
200 Adds this record to the database. If there is an error, returns the error,
201 otherwise returns false.
203 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
204 defined. An FS::cust_svc record will be created and inserted.
206 Currently available options are: I<jobnums>, I<child_objects> and
209 If I<jobnum> is set to an array reference, the jobnums of any export jobs will
210 be added to the referenced array.
212 If I<child_objects> is set to an array reference of FS::tablename objects
213 (for example, FS::svc_export_machine or FS::acct_snarf objects), they
214 will have their svcnum field set and will be inserted after this record,
215 but before any exports are run. Each element of the array can also
216 optionally be a two-element array reference containing the child object
217 and the name of an alternate field to be filled in with the newly-inserted
218 svcnum, for example C<[ $svc_forward, 'srcsvc' ]>
220 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
221 jobnums), all provisioning jobs will have a dependancy on the supplied
222 jobnum(s) (they will not run until the specific job(s) complete(s)).
224 If I<export_args> is set to an array reference, the referenced list will be
225 passed to export commands.
232 warn "[$me] insert called with options ".
233 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
237 local $FS::queue::jobnums = \@jobnums;
238 warn "[$me] insert: set \$FS::queue::jobnums to $FS::queue::jobnums\n"
240 my $objects = $options{'child_objects'} || [];
241 my $depend_jobnums = $options{'depend_jobnum'} || [];
242 $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
244 local $SIG{HUP} = 'IGNORE';
245 local $SIG{INT} = 'IGNORE';
246 local $SIG{QUIT} = 'IGNORE';
247 local $SIG{TERM} = 'IGNORE';
248 local $SIG{TSTP} = 'IGNORE';
249 local $SIG{PIPE} = 'IGNORE';
251 my $oldAutoCommit = $FS::UID::AutoCommit;
252 local $FS::UID::AutoCommit = 0;
255 my $svcnum = $self->svcnum;
256 my $cust_svc = $svcnum ? qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) : '';
257 my $inserted_cust_svc = 0;
258 #unless ( $svcnum ) {
259 if ( !$svcnum or !$cust_svc ) {
260 $cust_svc = new FS::cust_svc ( {
261 #hua?# 'svcnum' => $svcnum,
262 'svcnum' => $self->svcnum,
263 'pkgnum' => $self->pkgnum,
264 'svcpart' => $self->svcpart,
266 my $error = $cust_svc->insert;
268 $dbh->rollback if $oldAutoCommit;
271 $inserted_cust_svc = 1;
272 $svcnum = $self->svcnum($cust_svc->svcnum);
274 #$cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
275 unless ( $cust_svc ) {
276 $dbh->rollback if $oldAutoCommit;
277 return "no cust_svc record found for svcnum ". $self->svcnum;
279 $self->pkgnum($cust_svc->pkgnum);
280 $self->svcpart($cust_svc->svcpart);
283 my $error = $self->preinsert_hook_first
284 || $self->set_auto_inventory
286 || $self->_check_duplicate
287 || $self->preinsert_hook
288 || $self->SUPER::insert;
290 if ( $inserted_cust_svc ) {
291 my $derror = $cust_svc->delete;
292 die $derror if $derror;
294 $dbh->rollback if $oldAutoCommit;
298 foreach my $object ( @$objects ) {
300 if ( ref($object) eq 'ARRAY' ) {
301 ($obj, $field) = @$object;
306 $obj->$field($self->svcnum);
307 $error = $obj->insert;
309 $dbh->rollback if $oldAutoCommit;
315 unless ( $noexport_hack ) {
317 warn "[$me] insert: \$FS::queue::jobnums is $FS::queue::jobnums\n"
320 my $export_args = $options{'export_args'} || [];
322 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
323 my $error = $part_export->export_insert($self, @$export_args);
325 $dbh->rollback if $oldAutoCommit;
326 return "exporting to ". $part_export->exporttype.
327 " (transaction rolled back): $error";
331 foreach my $depend_jobnum ( @$depend_jobnums ) {
332 warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
334 foreach my $jobnum ( @jobnums ) {
335 my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
336 warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
338 my $error = $queue->depend_insert($depend_jobnum);
340 $dbh->rollback if $oldAutoCommit;
341 return "error queuing job dependancy: $error";
348 my $nms_ip_error = $self->nms_ip_insert;
349 if ( $nms_ip_error ) {
350 $dbh->rollback if $oldAutoCommit;
351 return "error queuing IP insert: $nms_ip_error";
354 if ( exists $options{'jobnums'} ) {
355 push @{ $options{'jobnums'} }, @jobnums;
358 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
364 sub preinsert_hook_first { ''; }
365 sub _check_duplcate { ''; }
366 sub preinsert_hook { ''; }
367 sub table_dupcheck_fields { (); }
368 sub prereplace_hook { ''; }
369 sub prereplace_hook_first { ''; }
370 sub predelete_hook { ''; }
371 sub predelete_hook_first { ''; }
373 =item delete [ , OPTION => VALUE ... ]
375 Deletes this account from the database. If there is an error, returns the
376 error, otherwise returns false.
378 The corresponding FS::cust_svc record will be deleted as well.
385 my $export_args = $options{'export_args'} || [];
387 local $SIG{HUP} = 'IGNORE';
388 local $SIG{INT} = 'IGNORE';
389 local $SIG{QUIT} = 'IGNORE';
390 local $SIG{TERM} = 'IGNORE';
391 local $SIG{TSTP} = 'IGNORE';
392 local $SIG{PIPE} = 'IGNORE';
394 my $oldAutoCommit = $FS::UID::AutoCommit;
395 local $FS::UID::AutoCommit = 0;
398 my $error = $self->cust_svc->check_part_svc_link_unprovision
399 || $self->predelete_hook_first
400 || $self->SUPER::delete
401 || $self->export('delete', @$export_args)
402 || $self->return_inventory
403 || $self->release_router
404 || $self->predelete_hook
405 || $self->cust_svc->delete
408 $dbh->rollback if $oldAutoCommit;
412 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
419 Currently this will only run expire exports if any are attached
424 my($self,$date) = (shift,shift);
426 return 'Expire date must be specified' unless $date;
428 local $SIG{HUP} = 'IGNORE';
429 local $SIG{INT} = 'IGNORE';
430 local $SIG{QUIT} = 'IGNORE';
431 local $SIG{TERM} = 'IGNORE';
432 local $SIG{TSTP} = 'IGNORE';
433 local $SIG{PIPE} = 'IGNORE';
435 my $oldAutoCommit = $FS::UID::AutoCommit;
436 local $FS::UID::AutoCommit = 0;
439 my $export_args = [$date];
440 my $error = $self->export('expire', @$export_args);
442 $dbh->rollback if $oldAutoCommit;
446 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
451 =item replace [ OLD_RECORD ] [ HASHREF | OPTION => VALUE ]
453 Replaces OLD_RECORD with this one. If there is an error, returns the error,
454 otherwise returns false.
456 Currently available options are: I<child_objects>, I<export_args> and
459 If I<child_objects> is set to an array reference of FS::tablename objects
460 (for example, FS::svc_export_machine or FS::acct_snarf objects), they
461 will have their svcnum field set and will be inserted or replaced after
462 this record, but before any exports are run. Each element of the array
463 can also optionally be a two-element array reference containing the
464 child object and the name of an alternate field to be filled in with
465 the newly-inserted svcnum, for example C<[ $svc_forward, 'srcsvc' ]>
467 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
468 jobnums), all provisioning jobs will have a dependancy on the supplied
469 jobnum(s) (they will not run until the specific job(s) complete(s)).
471 If I<export_args> is set to an array reference, the referenced list will be
472 passed to export commands.
479 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
484 ( ref($_[0]) eq 'HASH' )
488 my $objects = $options->{'child_objects'} || [];
491 local $FS::queue::jobnums = \@jobnums;
492 warn "[$me] replace: set \$FS::queue::jobnums to $FS::queue::jobnums\n"
494 my $depend_jobnums = $options->{'depend_jobnum'} || [];
495 $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums);
497 local $SIG{HUP} = 'IGNORE';
498 local $SIG{INT} = 'IGNORE';
499 local $SIG{QUIT} = 'IGNORE';
500 local $SIG{TERM} = 'IGNORE';
501 local $SIG{TSTP} = 'IGNORE';
502 local $SIG{PIPE} = 'IGNORE';
504 my $oldAutoCommit = $FS::UID::AutoCommit;
505 local $FS::UID::AutoCommit = 0;
508 my $error = $new->prereplace_hook_first($old)
509 || $new->set_auto_inventory($old)
510 || $new->check; #redundant, but so any duplicate fields are
511 #maniuplated as appropriate (svc_phone.phonenum)
513 $dbh->rollback if $oldAutoCommit;
517 #if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
518 if ( grep { $old->$_ ne $new->$_ } $new->table_dupcheck_fields ) {
520 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
521 $error = $new->_check_duplicate;
523 $dbh->rollback if $oldAutoCommit;
528 $error = $new->SUPER::replace($old);
530 $dbh->rollback if $oldAutoCommit;
534 foreach my $object ( @$objects ) {
536 if ( ref($object) eq 'ARRAY' ) {
537 ($obj, $field) = @$object;
542 $obj->$field($new->svcnum);
544 my $oldobj = qsearchs( $obj->table, {
545 $field => $new->svcnum,
546 map { $_ => $obj->$_ } $obj->_svc_child_partfields,
550 my $pkey = $oldobj->primary_key;
551 $obj->$pkey($oldobj->$pkey);
552 $obj->replace($oldobj);
554 $error = $obj->insert;
557 $dbh->rollback if $oldAutoCommit;
563 unless ( $noexport_hack ) {
565 warn "[$me] replace: \$FS::queue::jobnums is $FS::queue::jobnums\n"
568 my $export_args = $options->{'export_args'} || [];
570 #not quite false laziness, but same pattern as FS::svc_acct::replace and
571 #FS::part_export::sqlradius::_export_replace. List::Compare or something
572 #would be useful but too much of a pain in the ass to deploy
574 my @old_part_export = $old->cust_svc->part_svc->part_export;
575 my %old_exportnum = map { $_->exportnum => 1 } @old_part_export;
576 my @new_part_export =
578 ? qsearchs('part_svc', { svcpart=>$new->svcpart } )->part_export
579 : $new->cust_svc->part_svc->part_export;
580 my %new_exportnum = map { $_->exportnum => 1 } @new_part_export;
582 foreach my $delete_part_export (
583 grep { ! $new_exportnum{$_->exportnum} } @old_part_export
585 my $error = $delete_part_export->export_delete($old, @$export_args);
587 $dbh->rollback if $oldAutoCommit;
588 return "error deleting, export to ". $delete_part_export->exporttype.
589 " (transaction rolled back): $error";
593 foreach my $replace_part_export (
594 grep { $old_exportnum{$_->exportnum} } @new_part_export
597 $replace_part_export->export_replace( $new, $old, @$export_args);
599 $dbh->rollback if $oldAutoCommit;
600 return "error exporting to ". $replace_part_export->exporttype.
601 " (transaction rolled back): $error";
605 foreach my $insert_part_export (
606 grep { ! $old_exportnum{$_->exportnum} } @new_part_export
608 my $error = $insert_part_export->export_insert($new, @$export_args );
610 $dbh->rollback if $oldAutoCommit;
611 return "error inserting export to ". $insert_part_export->exporttype.
612 " (transaction rolled back): $error";
616 foreach my $depend_jobnum ( @$depend_jobnums ) {
617 warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
619 foreach my $jobnum ( @jobnums ) {
620 my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
621 warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
623 my $error = $queue->depend_insert($depend_jobnum);
625 $dbh->rollback if $oldAutoCommit;
626 return "error queuing job dependancy: $error";
633 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
639 Sets any fixed fields for this service (see L<FS::part_svc>). If there is an
640 error, returns the error, otherwise returns the FS::part_svc object (use ref()
641 to test the return). Usually called by the check method.
647 $self->setx('F', @_);
652 Sets all fields to their defaults (see L<FS::part_svc>), overriding their
653 current values. If there is an error, returns the error, otherwise returns
654 the FS::part_svc object (use ref() to test the return).
660 $self->setx('D', @_ );
663 =item set_default_and_fixed
667 sub set_default_and_fixed {
669 $self->setx( [ 'D', 'F' ], @_ );
672 =item setx FLAG | FLAG_ARRAYREF , [ CALLBACK_HASHREF ]
674 Sets fields according to the passed in flag or arrayref of flags.
676 Optionally, a hashref of field names and callback coderefs can be passed.
677 If a coderef exists for a given field name, instead of setting the field,
678 the coderef is called with the column value (part_svc_column.columnvalue)
679 as the single parameter.
686 my @x = ref($x) ? @$x : ($x);
687 my $coderef = scalar(@_) ? shift : $self->_fieldhandlers;
690 $self->ut_numbern('svcnum')
692 return $error if $error;
694 my $part_svc = $self->part_svc;
695 return "Unknown svcpart" unless $part_svc;
697 #set default/fixed/whatever fields from part_svc
699 foreach my $part_svc_column (
700 grep { my $f = $_->columnflag; grep { $f eq $_ } @x } #columnflag in @x
701 $part_svc->all_part_svc_column
704 my $columnname = $part_svc_column->columnname;
705 my $columnvalue = $part_svc_column->columnvalue;
707 $columnvalue = &{ $coderef->{$columnname} }( $self, $columnvalue )
708 if exists( $coderef->{$columnname} );
709 $self->setfield( $columnname, $columnvalue );
722 if ( $self->get('svcpart') ) {
723 $svcpart = $self->get('svcpart');
724 } elsif ( $self->svcnum && qsearchs('cust_svc', {'svcnum'=>$self->svcnum}) ) {
725 my $cust_svc = $self->cust_svc;
726 return "Unknown svcnum" unless $cust_svc;
727 $svcpart = $cust_svc->svcpart;
730 qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
736 Returns the FS::svc_pbx record for this service, if any (see L<FS::svc_pbx>).
738 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
743 # XXX FS::h_svc_{acct,phone} could have a history-aware svc_pbx override
747 return '' unless $self->pbxsvc;
748 qsearchs( 'svc_pbx', { 'svcnum' => $self->pbxsvc } );
753 Returns the title of the FS::svc_pbx record associated with this service, if
756 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
763 my $svc_pbx = $self->svc_pbx or return '';
767 =item pbx_select_hash %OPTIONS
769 Can be called as an object method or a class method.
771 Returns a hash SVCNUM => TITLE ... representing the PBXes this customer
772 that may be associated with this service.
774 Currently available options are: I<pkgnum> I<svcpart>
776 Only makes sense if the service has a pbxsvc field (currently, svc_phone and
781 #false laziness w/svc_acct::domain_select_hash
782 sub pbx_select_hash {
783 my ($self, %options) = @_;
789 $part_svc = $self->part_svc;
790 $cust_pkg = $self->cust_svc->cust_pkg
794 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
795 if $options{'svcpart'};
797 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
798 if $options{'pkgnum'};
800 if ($part_svc && ( $part_svc->part_svc_column('pbxsvc')->columnflag eq 'S'
801 || $part_svc->part_svc_column('pbxsvc')->columnflag eq 'F')) {
802 %pbxes = map { $_->svcnum => $_->title }
803 map { qsearchs('svc_pbx', { 'svcnum' => $_ }) }
804 split(',', $part_svc->part_svc_column('pbxsvc')->columnvalue);
805 } elsif ($cust_pkg) { # && !$conf->exists('svc_acct-alldomains') ) {
806 %pbxes = map { $_->svcnum => $_->title }
807 map { qsearchs('svc_pbx', { 'svcnum' => $_->svcnum }) }
808 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
809 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
812 %pbxes = map { $_->svcnum => $_->title } qsearch('svc_pbx', {} );
815 if ($part_svc && $part_svc->part_svc_column('pbxsvc')->columnflag eq 'D') {
816 my $svc_pbx = qsearchs('svc_pbx',
817 { 'svcnum' => $part_svc->part_svc_column('pbxsvc')->columnvalue } );
819 $pbxes{$svc_pbx->svcnum} = $svc_pbx->title;
821 warn "unknown svc_pbx.svcnum for part_svc_column pbxsvc: ".
822 $part_svc->part_svc_column('pbxsvc')->columnvalue;
831 =item set_auto_inventory
833 Sets any fields which auto-populate from inventory (see L<FS::part_svc>), and
834 also check any manually populated inventory fields.
836 If there is an error, returns the error, otherwise returns false.
840 sub set_auto_inventory {
841 # don't try to do this during an upgrade
842 return '' if $FS::CurrentUser::upgrade_hack;
845 my $old = @_ ? shift : '';
848 $self->ut_numbern('svcnum')
850 return $error if $error;
852 my $part_svc = $self->part_svc;
853 return "Unkonwn svcpart" unless $part_svc;
855 local $SIG{HUP} = 'IGNORE';
856 local $SIG{INT} = 'IGNORE';
857 local $SIG{QUIT} = 'IGNORE';
858 local $SIG{TERM} = 'IGNORE';
859 local $SIG{TSTP} = 'IGNORE';
860 local $SIG{PIPE} = 'IGNORE';
862 my $oldAutoCommit = $FS::UID::AutoCommit;
863 local $FS::UID::AutoCommit = 0;
866 #set default/fixed/whatever fields from part_svc
867 my $table = $self->table;
868 foreach my $field ( grep { $_ ne 'svcnum' } $self->fields ) {
870 my $part_svc_column = $part_svc->part_svc_column($field);
871 my $columnflag = $part_svc_column->columnflag;
872 next unless $columnflag =~ /^[AM]$/;
874 next if $columnflag eq 'A' && $self->$field() ne '';
876 my $classnum = $part_svc_column->columnvalue;
879 if ( $columnflag eq 'A' && $self->$field() eq '' ) {
880 $hash{'svcnum'} = '';
881 } elsif ( $columnflag eq 'M' ) {
882 return "Select inventory item for $field" unless $self->getfield($field);
883 $hash{'item'} = $self->getfield($field);
884 my $chosen_classnum = $self->getfield($field.'_classnum');
885 if ( grep {$_ == $chosen_classnum} split(',', $classnum) ) {
886 $classnum = $chosen_classnum;
888 # otherwise the chosen classnum is either (all), or somehow not on
889 # the list, so ignore it and choose the first item that's in any
893 my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql(
895 'table' => 'inventory_item',
898 my $inventory_item = qsearchs({
899 'table' => 'inventory_item',
901 'extra_sql' => "AND classnum IN ($classnum) AND $agentnums_sql",
902 'order_by' => 'ORDER BY ( agentnum IS NULL ) '. #agent inventory first
903 ' LIMIT 1 FOR UPDATE',
906 unless ( $inventory_item ) {
907 # should really only be shown if columnflag eq 'A'...
908 $dbh->rollback if $oldAutoCommit;
909 my $message = 'Out of ';
910 my @classnums = split(',', $classnum);
911 foreach ( @classnums ) {
912 my $class = FS::inventory_class->by_key($_)
913 or return "Can't find inventory_class.classnum $_";
914 $message .= PL_N($class->classname);
915 if ( scalar(@classnums) > 2 ) { # english is hard
916 if ( $_ != $classnums[-1] ) {
920 if ( scalar(@classnums) > 1 and $_ == $classnums[-2] ) {
927 next if $columnflag eq 'M' && $inventory_item->svcnum == $self->svcnum;
929 $self->setfield( $field, $inventory_item->item );
930 #if $columnflag eq 'A' && $self->$field() eq '';
932 # release the old inventory item, if there was one
933 if ( $old && $old->$field() && $old->$field() ne $self->$field() ) {
934 my $old_inv = qsearchs({
935 'table' => 'inventory_item',
937 'svcnum' => $old->svcnum,
939 'extra_sql' => "AND classnum IN ($classnum) AND ".
940 '( ( svc_field IS NOT NULL AND svc_field = '.$dbh->quote($field).' )'.
941 ' OR ( svc_field IS NULL AND item = '. dbh->quote($old->$field).' )'.
945 $old_inv->svcnum('');
946 $old_inv->svc_field('');
947 my $oerror = $old_inv->replace;
949 $dbh->rollback if $oldAutoCommit;
950 return "Error unprovisioning inventory: $oerror";
953 warn "old inventory_item not found for $field ". $self->$field;
957 $inventory_item->svcnum( $self->svcnum );
958 $inventory_item->svc_field( $field );
959 my $ierror = $inventory_item->replace();
961 $dbh->rollback if $oldAutoCommit;
962 return "Error provisioning inventory: $ierror";
967 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
973 =item return_inventory
975 Release all inventory items attached to this service's fields. Call
976 when unprovisioning the service.
980 sub return_inventory {
983 local $SIG{HUP} = 'IGNORE';
984 local $SIG{INT} = 'IGNORE';
985 local $SIG{QUIT} = 'IGNORE';
986 local $SIG{TERM} = 'IGNORE';
987 local $SIG{TSTP} = 'IGNORE';
988 local $SIG{PIPE} = 'IGNORE';
990 my $oldAutoCommit = $FS::UID::AutoCommit;
991 local $FS::UID::AutoCommit = 0;
994 foreach my $inventory_item ( $self->inventory_item ) {
995 $inventory_item->svcnum('');
996 $inventory_item->svc_field('');
997 my $error = $inventory_item->replace();
999 $dbh->rollback if $oldAutoCommit;
1000 return "Error returning inventory: $error";
1004 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1009 =item inventory_item
1011 Returns the inventory items associated with this svc_ record, as
1012 FS::inventory_item objects (see L<FS::inventory_item>.
1016 sub inventory_item {
1019 'table' => 'inventory_item',
1020 'hashref' => { 'svcnum' => $self->svcnum, },
1024 =item release_router
1026 Delete any routers associated with this service. This will release their
1027 address blocks, also.
1031 sub release_router {
1033 my @routers = qsearch('router', { svcnum => $self->svcnum });
1034 foreach (@routers) {
1035 my $error = $_->delete;
1036 return "$error (removing router '".$_->routername."')" if $error;
1044 Returns the cust_svc record associated with this svc_ record, as a FS::cust_svc
1045 object (see L<FS::cust_svc>).
1049 Runs export_suspend callbacks.
1056 my $export_args = $options{'export_args'} || [];
1057 $self->export('suspend', @$export_args);
1062 Runs export_unsuspend callbacks.
1069 my $export_args = $options{'export_args'} || [];
1070 $self->export('unsuspend', @$export_args);
1075 Runs export_links callbacks and returns the links.
1082 $self->export('links', $return);
1086 =item export_getsettings
1088 Runs export_getsettings callbacks and returns the two hashrefs.
1092 sub export_getsettings {
1096 my $error = $self->export('getsettings', \%settings, \%defaults);
1098 warn "error running export_getsetings: $error";
1099 return ( { 'error' => $error }, {} );
1101 ( \%settings, \%defaults );
1104 =item export_getstatus
1106 Runs export_getstatus callbacks and returns a two item list consisting of an
1107 HTML status and a status hashref.
1111 sub export_getstatus {
1115 my $error = $self->export('getstatus', \$html, \%hash);
1117 warn "error running export_getstatus: $error";
1118 return ( '', { 'error' => $error } );
1123 =item export_setstatus
1125 Runs export_setstatus callbacks. If there is an error, returns the error,
1126 otherwise returns false.
1130 sub export_setstatus { shift->_export_setstatus_X('setstatus', @_) }
1131 sub export_setstatus_listadd { shift->_export_setstatus_X('setstatus_listadd', @_) }
1132 sub export_setstatus_listdel { shift->_export_setstatus_X('setstatus_listdel', @_) }
1133 sub export_setstatus_vacationadd { shift->_export_setstatus_X('setstatus_vacationadd', @_) }
1134 sub export_setstatus_vacationdel { shift->_export_setstatus_X('setstatus_vacationdel', @_) }
1136 sub _export_setstatus_X {
1137 my( $self, $method, @args ) = @_;
1138 my $error = $self->export($method, @args);
1140 warn "error running export_$method: $error";
1146 =item export HOOK [ EXPORT_ARGS ]
1148 Runs the provided export hook (i.e. "suspend", "unsuspend") for this service.
1153 my( $self, $method ) = ( shift, shift );
1155 $method = "export_$method" unless $method =~ /^export_/;
1157 local $SIG{HUP} = 'IGNORE';
1158 local $SIG{INT} = 'IGNORE';
1159 local $SIG{QUIT} = 'IGNORE';
1160 local $SIG{TERM} = 'IGNORE';
1161 local $SIG{TSTP} = 'IGNORE';
1162 local $SIG{PIPE} = 'IGNORE';
1164 my $oldAutoCommit = $FS::UID::AutoCommit;
1165 local $FS::UID::AutoCommit = 0;
1169 unless ( $noexport_hack ) {
1170 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1171 next unless $part_export->can($method);
1172 my $error = $part_export->$method($self, @_);
1174 $dbh->rollback if $oldAutoCommit;
1175 return "error exporting $method event to ". $part_export->exporttype.
1176 " (transaction rolled back): $error";
1181 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1188 Sets or retrieves overlimit date.
1194 #$self->cust_svc->overlimit(@_);
1195 my $cust_svc = $self->cust_svc;
1196 unless ( $cust_svc ) { #wtf?
1197 my $error = "$me overlimit: missing cust_svc record for svc_acct svcnum ".
1199 if ( $overlimit_missing_cust_svc_nonfatal_kludge ) {
1200 cluck "$error; continuing anyway as requested";
1206 $cust_svc->overlimit(@_);
1211 Stub - returns false (no error) so derived classes don't need to define this
1212 methods. Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
1214 This method is called *before* the deletion step which actually deletes the
1215 services. This method should therefore only be used for "pre-deletion"
1216 cancellation steps, if necessary.
1222 =item clone_suspended
1224 Constructor used by FS::part_export::_export_suspend fallback. Stub returning
1225 same object for svc_ classes which don't implement a suspension fallback
1226 (everything except svc_acct at the moment). Document better.
1230 sub clone_suspended {
1234 =item clone_kludge_unsuspend
1236 Constructor used by FS::part_export::_export_unsuspend fallback. Stub returning
1237 same object for svc_ classes which don't implement a suspension fallback
1238 (everything except svc_acct at the moment). Document better.
1242 sub clone_kludge_unsuspend {
1246 =item find_duplicates MODE FIELDS...
1248 Method used by _check_duplicate routines to find services with duplicate
1249 values in specified fields. Set MODE to 'global' to search across all
1250 services, or 'export' to limit to those that share one or more exports
1251 with this service. FIELDS is a list of field names; only services
1252 matching in all fields will be returned. Empty fields will be skipped.
1256 sub find_duplicates {
1261 my %search = map { $_ => $self->getfield($_) }
1262 grep { length($self->getfield($_)) } @fields;
1263 return () if !%search;
1264 my @dup = grep { ! $self->svcnum or $_->svcnum != $self->svcnum }
1265 qsearch( $self->table, \%search );
1267 return @dup if $mode eq 'global';
1268 die "incorrect find_duplicates mode '$mode'" if $mode ne 'export';
1270 my $exports = FS::part_export::export_info($self->table);
1271 my %conflict_svcparts;
1272 my $part_svc = $self->part_svc;
1273 foreach my $part_export ( $part_svc->part_export ) {
1274 %conflict_svcparts = map { $_->svcpart => 1 } $part_export->export_svc;
1276 return grep { $conflict_svcparts{$_->cust_svc->svcpart} } @dup;
1279 =item getstatus_html
1283 sub getstatus_html {
1286 my $part_svc = $self->cust_svc->part_svc;
1290 foreach my $export ( grep $_->can('export_getstatus'), $part_svc->part_export ) {
1291 my $export_html = '';
1293 $export->export_getstatus( $self, \$export_html, \%hash );
1294 $html .= $export_html;
1307 my $conf = new FS::Conf;
1308 return '' unless grep { $self->table eq $_ }
1309 $conf->config('nms-auto_add-svc_ips');
1310 my $ip_field = $self->table_info->{'ip_field'};
1312 my $queue = FS::queue->new( {
1313 'job' => 'FS::NetworkMonitoringSystem::queued_add_router',
1314 'svcnum' => $self->svcnum,
1316 $queue->insert( 'FS::NetworkMonitoringSystem',
1318 $conf->config('nms-auto_add-community')
1327 #XXX not yet implemented
1330 =item search_sql_field FIELD STRING
1332 Class method which returns an SQL fragment to search for STRING in FIELD.
1334 It is now case-insensitive by default.
1338 sub search_sql_field {
1339 my( $class, $field, $string ) = @_;
1340 my $table = $class->table;
1341 my $q_string = dbh->quote($string);
1342 "LOWER($table.$field) = LOWER($q_string)";
1345 #fallback for services that don't provide a search...
1347 #my( $class, $string ) = @_;
1351 =item search HASHREF
1353 Class method which returns a qsearch hash expression to search for parameters
1354 specified in HASHREF.
1360 =item unlinked - set to search for all unlinked services. Overrides all other options.
1370 =item pkgpart - arrayref
1372 =item routernum - arrayref
1374 =item sectornum - arrayref
1376 =item towernum - arrayref
1384 # svc_broadband::search should eventually use this instead
1386 my ($class, $params) = @_;
1389 'LEFT JOIN cust_svc USING ( svcnum )',
1390 'LEFT JOIN part_svc USING ( svcpart )',
1391 'LEFT JOIN cust_pkg USING ( pkgnum )',
1392 FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'),
1397 $class->_search_svc($params, \@from, \@where) if $class->can('_search_svc');
1400 # if ( $params->{'domain'} ) {
1401 # my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
1402 # #preserve previous behavior & bubble up an error if $svc_domain not found?
1403 # push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
1407 # if ( $params->{'domsvc'} =~ /^(\d+)$/ ) {
1408 # push @where, "domsvc = $1";
1412 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
1415 if ( $params->{'agentnum'} =~ /^(\d+)$/ && $1 ) {
1416 push @where, "cust_main.agentnum = $1";
1420 if ( $params->{'custnum'} =~ /^(\d+)$/ && $1 ) {
1421 push @where, "cust_pkg.custnum = $1";
1425 if ( $params->{'cust_status'} =~ /^([a-z]+)$/ ) {
1426 push @where, FS::cust_main->cust_status_sql . " = '$1'";
1430 if ( $params->{'balance'} =~ /^\s*(\-?\d*(\.\d{1,2})?)\s*$/ && length($1) ) {
1434 if ( $params->{'balance_days'} =~ /^\s*(\d*(\.\d{1,3})?)\s*$/ && length($1) ) {
1435 $age = time - 86400 * $1;
1437 push @where, FS::cust_main->balance_date_sql($age) . " > $balance";
1441 if ( $params->{'payby'} && scalar(@{ $params->{'payby'} }) ) {
1442 my @payby = map "'$_'", grep /^(\w+)$/, @{ $params->{'payby'} };
1443 push @where, 'payby IN ('. join(',', @payby ). ')';
1447 ##pkgpart, now properly untainted, can be arrayref
1448 #for my $pkgpart ( $params->{'pkgpart'} ) {
1449 # if ( ref $pkgpart ) {
1450 # my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
1451 # push @where, "cust_pkg.pkgpart IN ($where)" if $where;
1453 # elsif ( $pkgpart =~ /^(\d+)$/ ) {
1454 # push @where, "cust_pkg.pkgpart = $1";
1457 if ( $params->{'pkgpart'} ) {
1458 my @pkgpart = ref( $params->{'pkgpart'} )
1459 ? @{ $params->{'pkgpart'} }
1460 : $params->{'pkgpart'}
1461 ? ( $params->{'pkgpart'} )
1463 @pkgpart = grep /^(\d+)$/, @pkgpart;
1464 push @where, 'cust_pkg.pkgpart IN ('. join(',', @pkgpart ). ')' if @pkgpart;
1468 if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
1469 push @where, "svcnum = $1";
1473 if ( $params->{'svcpart'} ) {
1474 my @svcpart = ref( $params->{'svcpart'} )
1475 ? @{ $params->{'svcpart'} }
1476 : $params->{'svcpart'}
1477 ? ( $params->{'svcpart'} )
1479 @svcpart = grep /^(\d+)$/, @svcpart;
1480 push @where, 'svcpart IN ('. join(',', @svcpart ). ')' if @svcpart;
1483 if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
1484 push @from, ' LEFT JOIN export_svc USING ( svcpart )';
1485 push @where, "exportnum = $1";
1488 # # sector and tower
1489 # my @where_sector = $class->tower_sector_sql($params);
1490 # if ( @where_sector ) {
1491 # push @where, @where_sector;
1492 # push @from, ' LEFT JOIN tower_sector USING ( sectornum )';
1495 # here is the agent virtualization
1496 #if ($params->{CurrentUser}) {
1498 # qsearchs('access_user', { username => $params->{CurrentUser} });
1500 # if ($access_user) {
1501 # push @where, $access_user->agentnums_sql('table'=>'cust_main');
1503 # push @where, "1=0";
1506 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
1507 'table' => 'cust_main',
1508 'null_right' => 'View/link unlinked services',
1512 push @where, @{ $params->{'where'} } if $params->{'where'};
1514 my $addl_from = join(' ', @from);
1515 my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
1517 my $table = $class->table;
1519 my $count_query = "SELECT COUNT(*) FROM $table $addl_from $extra_sql";
1520 #if ( keys %svc_X ) {
1521 # $count_query .= ' WHERE '.
1522 # join(' AND ', map "$_ = ". dbh->quote($svc_X{$_}),
1530 'select' => join(', ',
1533 'cust_main.custnum',
1534 @{ $params->{'addl_select'} || [] },
1535 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
1537 'addl_from' => $addl_from,
1538 'extra_sql' => $extra_sql,
1539 'order_by' => $params->{'order_by'},
1540 'count_query' => $count_query,
1549 The setfixed method return value.
1551 B<export> method isn't used by insert and replace methods yet.
1555 L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, schema.html
1556 from the base documentation.