X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fsvc_Common.pm;h=9137c3f69a6fc79485efa4bcaacc75bfb7f4a32f;hp=2fed5dd8a57408b68de6a064db33ac89eabfdbb8;hb=aed8ec35ccb9cdeb7ea0cb6ff2946f9d83d582f6;hpb=9ae101389f2fe652575c6ab314a5e95c2283b72e diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm index 2fed5dd8a..9137c3f69 100644 --- a/FS/FS/svc_Common.pm +++ b/FS/FS/svc_Common.pm @@ -1,13 +1,27 @@ package FS::svc_Common; use strict; -use vars qw( @ISA $noexport_hack ); +use vars qw( @ISA $noexport_hack $DEBUG $me + $overlimit_missing_cust_svc_nonfatal_kludge ); +use Carp qw( cluck carp croak confess ); #specify cluck have to specify them all +use Scalar::Util qw( blessed ); +use FS::Conf; use FS::Record qw( qsearch qsearchs fields dbh ); +use FS::cust_main_Mixin; use FS::cust_svc; use FS::part_svc; use FS::queue; +use FS::cust_main; +use FS::inventory_item; +use FS::inventory_class; +use FS::NetworkMonitoringSystem; -@ISA = qw( FS::Record ); +@ISA = qw( FS::cust_main_Mixin FS::Record ); + +$me = '[FS::svc_Common]'; +$DEBUG = 0; + +$overlimit_missing_cust_svc_nonfatal_kludge = 0; =head1 NAME @@ -28,8 +42,69 @@ inherit from, i.e. FS::svc_acct. FS::svc_Common inherits from FS::Record. =over 4 +=item search_sql_field FIELD STRING + +Class method which returns an SQL fragment to search for STRING in FIELD. + +It is now case-insensitive by default. + =cut +sub search_sql_field { + my( $class, $field, $string ) = @_; + my $table = $class->table; + my $q_string = dbh->quote($string); + "LOWER($table.$field) = LOWER($q_string)"; +} + +#fallback for services that don't provide a search... +sub search_sql { + #my( $class, $string ) = @_; + '1 = 0'; #false +} + +=item new + +=cut + +sub new { + my $proto = shift; + my $class = ref($proto) || $proto; + my $self = {}; + bless ($self, $class); + + unless ( defined ( $self->table ) ) { + $self->{'Table'} = shift; + carp "warning: FS::Record::new called with table name ". $self->{'Table'}; + } + + #$self->{'Hash'} = shift; + my $newhash = shift; + $self->{'Hash'} = { map { $_ => $newhash->{$_} } qw(svcnum svcpart) }; + + $self->setdefault( $self->_fieldhandlers ) + unless $self->svcnum; + + $self->{'Hash'}{$_} = $newhash->{$_} + foreach grep { defined($newhash->{$_}) && length($newhash->{$_}) } + keys %$newhash; + + foreach my $field ( grep !defined($self->{'Hash'}{$_}), $self->fields ) { + $self->{'Hash'}{$field}=''; + } + + $self->_rebless if $self->can('_rebless'); + + $self->{'modified'} = 0; + + $self->_cache($self->{'Hash'}, shift) if $self->can('_cache') && @_; + + $self; +} + +#empty default +sub _fieldhandlers { {}; } + sub virtual_fields { # This restricts the fields based on part_svc_column and the svcpart of @@ -49,8 +124,10 @@ sub virtual_fields { if ($self->svcpart) { # Case 1 $svcpart = $self->svcpart; - } elsif (my $cust_svc = $self->cust_svc) { # Case 2 - $svcpart = $cust_svc->svcpart; + } elsif ( $self->svcnum + && qsearchs('cust_svc',{'svcnum'=>$self->svcnum} ) + ) { #Case 2 + $svcpart = $self->cust_svc->svcpart; } else { # Case 3 $svcpart = ''; } @@ -59,13 +136,41 @@ sub virtual_fields { my %flags = map { $_->columnname, $_->columnflag } ( qsearch ('part_svc_column', { svcpart => $svcpart } ) ); - return grep { not ($flags{$_} eq 'X') } @vfields; + return grep { not ( defined($flags{$_}) && $flags{$_} eq 'X') } @vfields; } else { # Case 3 return @vfields; } return (); } +=item label + +svc_Common provides a fallback label subroutine that just returns the svcnum. + +=cut + +sub label { + my $self = shift; + cluck "warning: ". ref($self). " not loaded or missing label method; ". + "using svcnum"; + $self->svcnum; +} + +sub label_long { + my $self = shift; + $self->label(@_); +} + +sub cust_main { + my $self = shift; + (($self->cust_svc || return)->cust_pkg || return)->cust_main || return +} + +sub cust_linked { + my $self = shift; + defined($self->cust_main); +} + =item check Checks the validity of fields in this record. @@ -80,7 +185,7 @@ sub check { $self->SUPER::check; } -=item insert [ JOBNUM_ARRAYREF [ OBJECTS_ARRAYREF ] ] +=item insert [ , OPTION => VALUE ... ] Adds this record to the database. If there is an error, returns the error, otherwise returns false. @@ -88,20 +193,42 @@ otherwise returns false. The additional fields pkgnum and svcpart (see L) should be defined. An FS::cust_svc record will be created and inserted. -If an arrayref is passed as parameter, the Bs of any export jobs will -be added to the array. +Currently available options are: I, I and +I. + +If I is set to an array reference, the jobnums of any export jobs will +be added to the referenced array. -If an arrayref of FS::tablename objects (for example, FS::acct_snarf objects) -is passed as the optional second parameter, they will have their svcnum fields -set and will be inserted after this record, but before any exports are run. +If I is set to an array reference of FS::tablename objects (for +example, FS::acct_snarf objects), they will have their svcnum field set and +will be inserted after this record, but before any exports are run. Each +element of the array can also optionally be a two-element array reference +containing the child object and the name of an alternate field to be filled in +with the newly-inserted svcnum, for example C<[ $svc_forward, 'srcsvc' ]> + +If I is set (to a scalar jobnum or an array reference of +jobnums), all provisioning jobs will have a dependancy on the supplied +jobnum(s) (they will not run until the specific job(s) complete(s)). + +If I is set to an array reference, the referenced list will be +passed to export commands. =cut sub insert { my $self = shift; - local $FS::queue::jobnums = shift if @_; - my $objects = scalar(@_) ? shift : []; - my $error; + my %options = @_; + warn "[$me] insert called with options ". + join(', ', map { "$_: $options{$_}" } keys %options ). "\n" + if $DEBUG; + + my @jobnums = (); + local $FS::queue::jobnums = \@jobnums; + warn "[$me] insert: set \$FS::queue::jobnums to $FS::queue::jobnums\n" + if $DEBUG; + my $objects = $options{'child_objects'} || []; + my $depend_jobnums = $options{'depend_jobnum'} || []; + $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums); local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -114,25 +241,24 @@ sub insert { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - $error = $self->check; - return $error if $error; - my $svcnum = $self->svcnum; - my $cust_svc; - unless ( $svcnum ) { + my $cust_svc = $svcnum ? qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) : ''; + #unless ( $svcnum ) { + if ( !$svcnum or !$cust_svc ) { $cust_svc = new FS::cust_svc ( { #hua?# 'svcnum' => $svcnum, + 'svcnum' => $self->svcnum, 'pkgnum' => $self->pkgnum, 'svcpart' => $self->svcpart, } ); - $error = $cust_svc->insert; + my $error = $cust_svc->insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; } $svcnum = $self->svcnum($cust_svc->svcnum); } else { - $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum}); + #$cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum}); unless ( $cust_svc ) { $dbh->rollback if $oldAutoCommit; return "no cust_svc record found for svcnum ". $self->svcnum; @@ -141,15 +267,27 @@ sub insert { $self->svcpart($cust_svc->svcpart); } - $error = $self->SUPER::insert; + my $error = $self->preinsert_hook_first + || $self->set_auto_inventory + || $self->check + || $self->_check_duplicate + || $self->preinsert_hook + || $self->SUPER::insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; } foreach my $object ( @$objects ) { - $object->svcnum($self->svcnum); - $error = $object->insert; + my($field, $obj); + if ( ref($object) eq 'ARRAY' ) { + ($obj, $field) = @$object; + } else { + $obj = $object; + $field = 'svcnum'; + } + $obj->$field($self->svcnum); + $error = $obj->insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -158,14 +296,46 @@ sub insert { #new-style exports! unless ( $noexport_hack ) { + + warn "[$me] insert: \$FS::queue::jobnums is $FS::queue::jobnums\n" + if $DEBUG; + + my $export_args = $options{'export_args'} || []; + foreach my $part_export ( $self->cust_svc->part_svc->part_export ) { - my $error = $part_export->export_insert($self); + my $error = $part_export->export_insert($self, @$export_args); if ( $error ) { $dbh->rollback if $oldAutoCommit; return "exporting to ". $part_export->exporttype. " (transaction rolled back): $error"; } } + + foreach my $depend_jobnum ( @$depend_jobnums ) { + warn "[$me] inserting dependancies on supplied job $depend_jobnum\n" + if $DEBUG; + foreach my $jobnum ( @jobnums ) { + my $queue = qsearchs('queue', { 'jobnum' => $jobnum } ); + warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n" + if $DEBUG; + my $error = $queue->depend_insert($depend_jobnum); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error queuing job dependancy: $error"; + } + } + } + + } + + my $nms_ip_error = $self->nms_ip_insert; + if ( $nms_ip_error ) { + $dbh->rollback if $oldAutoCommit; + return "error queuing IP insert: $nms_ip_error"; + } + + if ( exists $options{'jobnums'} ) { + push @{ $options{'jobnums'} }, @jobnums; } $dbh->commit or die $dbh->errstr if $oldAutoCommit; @@ -173,7 +343,15 @@ sub insert { ''; } -=item delete +#fallbacks +sub preinsert_hook_first { ''; } +sub _check_duplcate { ''; } +sub preinsert_hook { ''; } +sub table_dupcheck_fields { (); } +sub predelete_hook { ''; } +sub predelete_hook_first { ''; } + +=item delete [ , OPTION => VALUE ... ] Deletes this account from the database. If there is an error, returns the error, otherwise returns false. @@ -184,7 +362,8 @@ The corresponding FS::cust_svc record will be deleted as well. sub delete { my $self = shift; - my $error; + my %options = @_; + my $export_args = $options{'export_args'} || []; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -193,47 +372,95 @@ sub delete { local $SIG{TSTP} = 'IGNORE'; local $SIG{PIPE} = 'IGNORE'; - my $svcnum = $self->svcnum; - my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; - $error = $self->SUPER::delete; - return $error if $error; - - #new-style exports! - unless ( $noexport_hack ) { - foreach my $part_export ( $self->cust_svc->part_svc->part_export ) { - my $error = $part_export->export_delete($self); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "exporting to ". $part_export->exporttype. - " (transaction rolled back): $error"; - } - } + my $error = $self->predelete_hook_first + || $self->SUPER::delete + || $self->export('delete', @$export_args) + || $self->return_inventory + || $self->predelete_hook + || $self->cust_svc->delete + ; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; } - return $error if $error; + $dbh->commit or die $dbh->errstr if $oldAutoCommit; - my $cust_svc = $self->cust_svc; - $error = $cust_svc->delete; - return $error if $error; + ''; +} + +=item expire DATE + +Currently this will only run expire exports if any are attached + +=cut + +sub expire { + my($self,$date) = (shift,shift); + + return 'Expire date must be specified' unless $date; + + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $export_args = [$date]; + my $error = $self->export('expire', @$export_args); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; } -=item replace OLD_RECORD +=item replace [ OLD_RECORD ] [ HASHREF | OPTION => VALUE ] Replaces OLD_RECORD with this one. If there is an error, returns the error, otherwise returns false. +Currently available options are: I and I. + +If I is set (to a scalar jobnum or an array reference of +jobnums), all provisioning jobs will have a dependancy on the supplied +jobnum(s) (they will not run until the specific job(s) complete(s)). + +If I is set to an array reference, the referenced list will be +passed to export commands. + =cut sub replace { - my ($new, $old) = (shift, shift); + my $new = shift; + + my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') ) + ? shift + : $new->replace_old; + + my $options = + ( ref($_[0]) eq 'HASH' ) + ? shift + : { @_ }; + + my @jobnums = (); + local $FS::queue::jobnums = \@jobnums; + warn "[$me] replace: set \$FS::queue::jobnums to $FS::queue::jobnums\n" + if $DEBUG; + my $depend_jobnums = $options->{'depend_jobnum'} || []; + $depend_jobnums = [ $depend_jobnums ] unless ref($depend_jobnums); local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -246,7 +473,32 @@ sub replace { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $error = $new->SUPER::replace($old); + my $error = $new->set_auto_inventory($old); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + #redundant, but so any duplicate fields are maniuplated as appropriate + # (svc_phone.phonenum) + $error = $new->check; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + #if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) { + if ( grep { $old->$_ ne $new->$_ } $new->table_dupcheck_fields ) { + + $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart; + $error = $new->_check_duplicate; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + + $error = $new->SUPER::replace($old); if ($error) { $dbh->rollback if $oldAutoCommit; return $error; @@ -254,21 +506,79 @@ sub replace { #new-style exports! unless ( $noexport_hack ) { - foreach my $part_export ( $new->cust_svc->part_svc->part_export ) { - my $error = $part_export->export_replace($new,$old); + + warn "[$me] replace: \$FS::queue::jobnums is $FS::queue::jobnums\n" + if $DEBUG; + + my $export_args = $options->{'export_args'} || []; + + #not quite false laziness, but same pattern as FS::svc_acct::replace and + #FS::part_export::sqlradius::_export_replace. List::Compare or something + #would be useful but too much of a pain in the ass to deploy + + my @old_part_export = $old->cust_svc->part_svc->part_export; + my %old_exportnum = map { $_->exportnum => 1 } @old_part_export; + my @new_part_export = + $new->svcpart + ? qsearchs('part_svc', { svcpart=>$new->svcpart } )->part_export + : $new->cust_svc->part_svc->part_export; + my %new_exportnum = map { $_->exportnum => 1 } @new_part_export; + + foreach my $delete_part_export ( + grep { ! $new_exportnum{$_->exportnum} } @old_part_export + ) { + my $error = $delete_part_export->export_delete($old, @$export_args); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error deleting, export to ". $delete_part_export->exporttype. + " (transaction rolled back): $error"; + } + } + + foreach my $replace_part_export ( + grep { $old_exportnum{$_->exportnum} } @new_part_export + ) { + my $error = + $replace_part_export->export_replace( $new, $old, @$export_args); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error exporting to ". $replace_part_export->exporttype. + " (transaction rolled back): $error"; + } + } + + foreach my $insert_part_export ( + grep { ! $old_exportnum{$_->exportnum} } @new_part_export + ) { + my $error = $insert_part_export->export_insert($new, @$export_args ); if ( $error ) { $dbh->rollback if $oldAutoCommit; - return "error exporting to ". $part_export->exporttype. + return "error inserting export to ". $insert_part_export->exporttype. " (transaction rolled back): $error"; } } + + foreach my $depend_jobnum ( @$depend_jobnums ) { + warn "[$me] inserting dependancies on supplied job $depend_jobnum\n" + if $DEBUG; + foreach my $jobnum ( @jobnums ) { + my $queue = qsearchs('queue', { 'jobnum' => $jobnum } ); + warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n" + if $DEBUG; + my $error = $queue->depend_insert($depend_jobnum); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error queuing job dependancy: $error"; + } + } + } + } $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; } - =item setfixed Sets any fixed fields for this service (see L). If there is an @@ -279,7 +589,7 @@ to test the return). Usually called by the check method. sub setfixed { my $self = shift; - $self->setx('F'); + $self->setx('F', @_); } =item setdefault @@ -292,64 +602,295 @@ the FS::part_svc object (use ref() to test the return). sub setdefault { my $self = shift; - $self->setx('D'); + $self->setx('D', @_ ); } +=item set_default_and_fixed + +=cut + +sub set_default_and_fixed { + my $self = shift; + $self->setx( [ 'D', 'F' ], @_ ); +} + +=item setx FLAG | FLAG_ARRAYREF , [ CALLBACK_HASHREF ] + +Sets fields according to the passed in flag or arrayref of flags. + +Optionally, a hashref of field names and callback coderefs can be passed. +If a coderef exists for a given field name, instead of setting the field, +the coderef is called with the column value (part_svc_column.columnvalue) +as the single parameter. + +=cut + sub setx { my $self = shift; my $x = shift; + my @x = ref($x) ? @$x : ($x); + my $coderef = scalar(@_) ? shift : $self->_fieldhandlers; - my $error; - - $error = + my $error = $self->ut_numbern('svcnum') ; return $error if $error; + my $part_svc = $self->part_svc; + return "Unknown svcpart" unless $part_svc; + + #set default/fixed/whatever fields from part_svc + + foreach my $part_svc_column ( + grep { my $f = $_->columnflag; grep { $f eq $_ } @x } #columnflag in @x + $part_svc->all_part_svc_column + ) { + + my $columnname = $part_svc_column->columnname; + my $columnvalue = $part_svc_column->columnvalue; + + $columnvalue = &{ $coderef->{$columnname} }( $self, $columnvalue ) + if exists( $coderef->{$columnname} ); + $self->setfield( $columnname, $columnvalue ); + + } + + $part_svc; + +} + +sub part_svc { + my $self = shift; + #get part_svc my $svcpart; - if ( $self->svcnum ) { + if ( $self->get('svcpart') ) { + $svcpart = $self->get('svcpart'); + } elsif ( $self->svcnum && qsearchs('cust_svc', {'svcnum'=>$self->svcnum}) ) { my $cust_svc = $self->cust_svc; return "Unknown svcnum" unless $cust_svc; $svcpart = $cust_svc->svcpart; + } + + qsearchs( 'part_svc', { 'svcpart' => $svcpart } ); + +} + +=item svc_pbx + +Returns the FS::svc_pbx record for this service, if any (see L). + +Only makes sense if the service has a pbxsvc field (currently, svc_phone and +svc_acct). + +=cut + +# XXX FS::h_svc_{acct,phone} could have a history-aware svc_pbx override + +sub svc_pbx { + my $self = shift; + return '' unless $self->pbxsvc; + qsearchs( 'svc_pbx', { 'svcnum' => $self->pbxsvc } ); +} + +=item pbx_title + +Returns the title of the FS::svc_pbx record associated with this service, if +any. + +Only makes sense if the service has a pbxsvc field (currently, svc_phone and +svc_acct). + +=cut + +sub pbx_title { + my $self = shift; + my $svc_pbx = $self->svc_pbx or return ''; + $svc_pbx->title; +} + +=item pbx_select_hash %OPTIONS + +Can be called as an object method or a class method. + +Returns a hash SVCNUM => TITLE ... representing the PBXes this customer +that may be associated with this service. + +Currently available options are: I I + +Only makes sense if the service has a pbxsvc field (currently, svc_phone and +svc_acct). + +=cut + +#false laziness w/svc_acct::domain_select_hash +sub pbx_select_hash { + my ($self, %options) = @_; + my %pbxes = (); + my $part_svc; + my $cust_pkg; + + if (ref($self)) { + $part_svc = $self->part_svc; + $cust_pkg = $self->cust_svc->cust_pkg + if $self->cust_svc; + } + + $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} }) + if $options{'svcpart'}; + + $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} }) + if $options{'pkgnum'}; + + if ($part_svc && ( $part_svc->part_svc_column('pbxsvc')->columnflag eq 'S' + || $part_svc->part_svc_column('pbxsvc')->columnflag eq 'F')) { + %pbxes = map { $_->svcnum => $_->title } + map { qsearchs('svc_pbx', { 'svcnum' => $_ }) } + split(',', $part_svc->part_svc_column('pbxsvc')->columnvalue); + } elsif ($cust_pkg) { # && !$conf->exists('svc_acct-alldomains') ) { + %pbxes = map { $_->svcnum => $_->title } + map { qsearchs('svc_pbx', { 'svcnum' => $_->svcnum }) } + map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) } + qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum }); } else { - $svcpart = $self->getfield('svcpart'); + #XXX agent-virt + %pbxes = map { $_->svcnum => $_->title } qsearch('svc_pbx', {} ); } - my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $svcpart } ); + + if ($part_svc && $part_svc->part_svc_column('pbxsvc')->columnflag eq 'D') { + my $svc_pbx = qsearchs('svc_pbx', + { 'svcnum' => $part_svc->part_svc_column('pbxsvc')->columnvalue } ); + if ( $svc_pbx ) { + $pbxes{$svc_pbx->svcnum} = $svc_pbx->title; + } else { + warn "unknown svc_pbx.svcnum for part_svc_column pbxsvc: ". + $part_svc->part_svc_column('pbxsvc')->columnvalue; + + } + } + + (%pbxes); + +} + +=item set_auto_inventory + +Sets any fields which auto-populate from inventory (see L), and +also check any manually populated inventory fields. + +If there is an error, returns the error, otherwise returns false. + +=cut + +sub set_auto_inventory { + my $self = shift; + my $old = @_ ? shift : ''; + + my $error = + $self->ut_numbern('svcnum') + ; + return $error if $error; + + my $part_svc = $self->part_svc; return "Unkonwn svcpart" unless $part_svc; + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + #set default/fixed/whatever fields from part_svc my $table = $self->table; foreach my $field ( grep { $_ ne 'svcnum' } $self->fields ) { + my $part_svc_column = $part_svc->part_svc_column($field); - if ( $part_svc_column->columnflag eq $x ) { - $self->setfield( $field, $part_svc_column->columnvalue ); + my $columnflag = $part_svc_column->columnflag; + next unless $columnflag =~ /^[AM]$/; + + next if $columnflag eq 'A' && $self->$field() ne ''; + + my $classnum = $part_svc_column->columnvalue; + my %hash = ( 'classnum' => $classnum ); + + if ( $columnflag eq 'A' && $self->$field() eq '' ) { + $hash{'svcnum'} = ''; + } elsif ( $columnflag eq 'M' ) { + return "Select inventory item for $field" unless $self->getfield($field); + $hash{'item'} = $self->getfield($field); } - } - $part_svc; + my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql( + 'null' => 1, + 'table' => 'inventory_item', + ); -} + my $inventory_item = qsearchs({ + 'table' => 'inventory_item', + 'hashref' => \%hash, + 'extra_sql' => "AND $agentnums_sql", + 'order_by' => 'ORDER BY ( agentnum IS NULL ) '. #agent inventory first + ' LIMIT 1 FOR UPDATE', + }); -=item cust_svc + unless ( $inventory_item ) { + $dbh->rollback if $oldAutoCommit; + my $inventory_class = + qsearchs('inventory_class', { 'classnum' => $classnum } ); + return "Can't find inventory_class.classnum $classnum" + unless $inventory_class; + return "Out of ". $inventory_class->classname. "s\n"; #Lingua:: BS + #for pluralizing + } -Returns the cust_svc record associated with this svc_ record, as a FS::cust_svc -object (see L). + next if $columnflag eq 'M' && $inventory_item->svcnum == $self->svcnum; -=cut + $self->setfield( $field, $inventory_item->item ); + #if $columnflag eq 'A' && $self->$field() eq ''; -sub cust_svc { - my $self = shift; - qsearchs('cust_svc', { 'svcnum' => $self->svcnum } ); -} + $inventory_item->svcnum( $self->svcnum ); + my $ierror = $inventory_item->replace(); + if ( $ierror ) { + $dbh->rollback if $oldAutoCommit; + return "Error provisioning inventory: $ierror"; + } -=item suspend + if ( $old && $old->$field() && $old->$field() ne $self->$field() ) { + my $old_inv = qsearchs({ + 'table' => 'inventory_item', + 'hashref' => { 'classnum' => $classnum, + 'svcnum' => $old->svcnum, + 'item' => $old->$field(), + }, + }); + if ( $old_inv ) { + $old_inv->svcnum(''); + my $oerror = $old_inv->replace; + if ( $oerror ) { + $dbh->rollback if $oldAutoCommit; + return "Error unprovisioning inventory: $oerror"; + } + } + } -Runs export_suspend callbacks. + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ''; + +} + +=item return_inventory =cut -sub suspend { +sub return_inventory { my $self = shift; local $SIG{HUP} = 'IGNORE'; @@ -363,21 +904,58 @@ sub suspend { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - #new-style exports! - unless ( $noexport_hack ) { - foreach my $part_export ( $self->cust_svc->part_svc->part_export ) { - my $error = $part_export->export_suspend($self); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "error exporting to ". $part_export->exporttype. - " (transaction rolled back): $error"; - } + foreach my $inventory_item ( $self->inventory_item ) { + $inventory_item->svcnum(''); + my $error = $inventory_item->replace(); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error returning inventory: $error"; } } $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; +} + +=item inventory_item + +Returns the inventory items associated with this svc_ record, as +FS::inventory_item objects (see L. + +=cut + +sub inventory_item { + my $self = shift; + qsearch({ + 'table' => 'inventory_item', + 'hashref' => { 'svcnum' => $self->svcnum, }, + }); +} + +=item cust_svc + +Returns the cust_svc record associated with this svc_ record, as a FS::cust_svc +object (see L). + +=cut + +sub cust_svc { + my $self = shift; + qsearchs('cust_svc', { 'svcnum' => $self->svcnum } ); +} + +=item suspend + +Runs export_suspend callbacks. +=cut + +sub suspend { + my $self = shift; + my %options = @_; + my $export_args = $options{'export_args'} || []; + $self->export('suspend', @$export_args); } =item unsuspend @@ -388,6 +966,73 @@ Runs export_unsuspend callbacks. sub unsuspend { my $self = shift; + my %options = @_; + my $export_args = $options{'export_args'} || []; + $self->export('unsuspend', @$export_args); +} + +=item export_links + +Runs export_links callbacks and returns the links. + +=cut + +sub export_links { + my $self = shift; + my $return = []; + $self->export('links', $return); + $return; +} + +=item export_getsettings + +Runs export_getsettings callbacks and returns the two hashrefs. + +=cut + +sub export_getsettings { + my $self = shift; + my %settings = (); + my %defaults = (); + my $error = $self->export('getsettings', \%settings, \%defaults); + if ( $error ) { + #XXX bubble this up better + warn "error running export_getsetings: $error"; + return ( {}, {} ); + } + ( \%settings, \%defaults ); +} + +=item export_getstatus + +Runs export_getstatus callbacks and returns a two item list consisting of an +HTML status and a status hashref. + +=cut + +sub export_getstatus { + my $self = shift; + my $html = ''; + my %hash = (); + my $error = $self->export('getstatus', \$html, \%hash); + if ( $error ) { + #XXX bubble this up better + warn "error running export_getstatus: $error"; + return ( '', {} ); + } + ( $html, \%hash ); +} + +=item export HOOK [ EXPORT_ARGS ] + +Runs the provided export hook (i.e. "suspend", "unsuspend") for this service. + +=cut + +sub export { + my( $self, $method ) = ( shift, shift ); + + $method = "export_$method" unless $method =~ /^export_/; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -403,10 +1048,11 @@ sub unsuspend { #new-style exports! unless ( $noexport_hack ) { foreach my $part_export ( $self->cust_svc->part_svc->part_export ) { - my $error = $part_export->export_unsuspend($self); + next unless $part_export->can($method); + my $error = $part_export->$method($self, @_); if ( $error ) { $dbh->rollback if $oldAutoCommit; - return "error exporting to ". $part_export->exporttype. + return "error exporting $method event to ". $part_export->exporttype. " (transaction rolled back): $error"; } } @@ -417,25 +1063,158 @@ sub unsuspend { } +=item overlimit + +Sets or retrieves overlimit date. + +=cut + +sub overlimit { + my $self = shift; + #$self->cust_svc->overlimit(@_); + my $cust_svc = $self->cust_svc; + unless ( $cust_svc ) { #wtf? + my $error = "$me overlimit: missing cust_svc record for svc_acct svcnum ". + $self->svcnum; + if ( $overlimit_missing_cust_svc_nonfatal_kludge ) { + cluck "$error; continuing anyway as requested"; + return ''; + } else { + confess $error; + } + } + $cust_svc->overlimit(@_); +} + =item cancel -Stub - returns false (no error) so derived classes don't need to define these +Stub - returns false (no error) so derived classes don't need to define this methods. Called by the cancel method of FS::cust_pkg (see L). +This method is called *before* the deletion step which actually deletes the +services. This method should therefore only be used for "pre-deletion" +cancellation steps, if necessary. + =cut sub cancel { ''; } -=back +=item clone_suspended -=head1 VERSION +Constructor used by FS::part_export::_export_suspend fallback. Stub returning +same object for svc_ classes which don't implement a suspension fallback +(everything except svc_acct at the moment). Document better. -$Id: svc_Common.pm,v 1.14 2003-10-25 02:05:44 ivan Exp $ +=cut + +sub clone_suspended { + shift; +} + +=item clone_kludge_unsuspend + +Constructor used by FS::part_export::_export_unsuspend fallback. Stub returning +same object for svc_ classes which don't implement a suspension fallback +(everything except svc_acct at the moment). Document better. + +=cut + +sub clone_kludge_unsuspend { + shift; +} + +=item find_duplicates MODE FIELDS... + +Method used by _check_duplicate routines to find services with duplicate +values in specified fields. Set MODE to 'global' to search across all +services, or 'export' to limit to those that share one or more exports +with this service. FIELDS is a list of field names; only services +matching in all fields will be returned. Empty fields will be skipped. + +=cut + +sub find_duplicates { + my $self = shift; + my $mode = shift; + my @fields = @_; + + my %search = map { $_ => $self->getfield($_) } + grep { length($self->getfield($_)) } @fields; + return () if !%search; + my @dup = grep { ! $self->svcnum or $_->svcnum != $self->svcnum } + qsearch( $self->table, \%search ); + return () if !@dup; + return @dup if $mode eq 'global'; + die "incorrect find_duplicates mode '$mode'" if $mode ne 'export'; + + my $exports = FS::part_export::export_info($self->table); + my %conflict_svcparts; + my $part_svc = $self->part_svc; + foreach my $part_export ( $part_svc->part_export ) { + %conflict_svcparts = map { $_->svcpart => 1 } $part_export->export_svc; + } + return grep { $conflict_svcparts{$_->cust_svc->svcpart} } @dup; +} + +=item getstatus_html + +=cut + +sub getstatus_html { + my $self = shift; + + my $part_svc = $self->cust_svc->part_svc; + + my $html = ''; + + foreach my $export ( grep $_->can('export_getstatus'), $part_svc->part_export ) { + my $export_html = ''; + my %hash = (); + $export->export_getstatus( $self, \$export_html, \%hash ); + $html .= $export_html; + } + + $html; + +} + +=item nms_ip_insert + +=cut + +sub nms_ip_insert { + my $self = shift; + my $conf = new FS::Conf; + return '' unless grep { $self->table eq $_ } + $conf->config('nms-auto_add-svc_ips'); + my $ip_field = $self->table_info->{'ip_field'}; + + my $queue = FS::queue->new( { + 'job' => 'FS::NetworkMonitoringSystem::queued_add_router', + 'svcnum' => $self->svcnum, + } ); + $queue->insert( 'FS::NetworkMonitoringSystem', + $self->$ip_field(), + $conf->config('nms-auto_add-community') + ); +} + +=item nms_delip + +=cut + +sub nms_ip_delete { +#XXX not yet implemented +} + +=back =head1 BUGS The setfixed method return value. +B method isn't used by insert and replace methods yet. + =head1 SEE ALSO L, L, L, L, schema.html