use strict;
use vars qw( @ISA $DEBUG );
+use Tie::IxHash;
use FS::Record qw( qsearch qsearchs fields dbh );
use FS::Schema qw( dbdef );
use FS::part_svc_column;
@ISA = qw(FS::Record);
-$DEBUG = 1;
+$DEBUG = 0;
=head1 NAME
=item disabled - Disabled flag, empty or `Y'
+=item preserve - Preserve after cancellation, empty or 'Y'
+
=back
=head1 METHODS
=item I<svcdb>__I<field> - Default or fixed value for I<field> in I<svcdb>.
-=item I<svcdb>__I<field>_flag - defines I<svcdb>__I<field> action: null, `D' for default, or `F' for fixed. For virtual fields, can also be 'X' for excluded.
+=item I<svcdb>__I<field>_flag - defines I<svcdb>__I<field> action: null or empty (no default), `D' for default, `F' for fixed (unchangeable), `M' for manual selection from inventory, or `A' for automatic selection from inventory. For virtual fields, can also be 'X' for excluded.
=back
# fields('part_svc');
foreach my $field (
grep { $_ ne 'svcnum'
- && defined( $self->getfield($svcdb.'__'.$_.'_flag') )
+ && ( defined( $self->getfield($svcdb.'__'.$_.'_flag') )
+ || $self->getfield($svcdb.'__'.$_.'_label') !~ /^\s*$/ )
} (fields($svcdb), @fields)
) {
my $part_svc_column = $self->part_svc_column($field);
'columnname' => $field,
} );
- my $flag = $self->getfield($svcdb.'__'.$field.'_flag');
- if ( uc($flag) =~ /^([DFX])$/ ) {
- $part_svc_column->setfield('columnflag', $1);
- $part_svc_column->setfield('columnvalue',
- $self->getfield($svcdb.'__'.$field)
- );
+ my $flag = $self->getfield($svcdb.'__'.$field.'_flag');
+ my $label = $self->getfield($svcdb.'__'.$field.'_label');
+ if ( uc($flag) =~ /^([A-Z])$/ || $label !~ /^\s*$/ ) {
+
+ if ( uc($flag) =~ /^([A-Z])$/ ) {
+ my $parser = FS::part_svc->svc_table_fields($svcdb)->{$field}->{parse}
+ || sub { shift };
+ $part_svc_column->setfield('columnflag', $1);
+ $part_svc_column->setfield('columnvalue',
+ &$parser($self->getfield($svcdb.'__'.$field))
+ );
+ }
+
+ $part_svc_column->setfield('columnlabel', $label)
+ if $label !~ /^\s*$/;
+
if ( $previous ) {
$error = $part_svc_column->replace($previous);
} else {
$error = $part_svc_column->insert;
}
+
} else {
$error = $previous ? $previous->delete : '';
}
}
}
+ # XXX shouldn't this update fixed values?
+
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
my $svcdb = $new->svcdb;
foreach my $field (
grep { $_ ne 'svcnum'
- && defined( $new->getfield($svcdb.'__'.$_.'_flag') )
+ && ( defined( $new->getfield($svcdb.'__'.$_.'_flag') )
+ || $new->getfield($svcdb.'__'.$_.'_label') !~ /^\s*$/ )
} (fields($svcdb),@fields)
) {
+
my $part_svc_column = $new->part_svc_column($field);
my $previous = qsearchs('part_svc_column', {
'svcpart' => $new->svcpart,
'columnname' => $field,
} );
- my $flag = $new->getfield($svcdb.'__'.$field.'_flag');
- if ( uc($flag) =~ /^([DFX])$/ ) {
- $part_svc_column->setfield('columnflag', $1);
- $part_svc_column->setfield('columnvalue',
- $new->getfield($svcdb.'__'.$field)
- );
+ my $flag = $new->getfield($svcdb.'__'.$field.'_flag');
+ my $label = $new->getfield($svcdb.'__'.$field.'_label');
+
+ if ( uc($flag) =~ /^([A-Z])$/ || $label !~ /^\s*$/ ) {
+
+ if ( uc($flag) =~ /^([A-Z])$/ ) {
+ $part_svc_column->setfield('columnflag', $1);
+ my $parser = FS::part_svc->svc_table_fields($svcdb)->{$field}->{parse}
+ || sub { shift };
+ $part_svc_column->setfield('columnvalue',
+ &$parser($new->getfield($svcdb.'__'.$field))
+ );
+ } else {
+ $part_svc_column->setfield('columnflag', '');
+ $part_svc_column->setfield('columnvalue', '');
+ }
+
+ $part_svc_column->setfield('columnlabel', $label)
+ if $label !~ /^\s*$/;
+
if ( $previous ) {
$error = $part_svc_column->replace($previous);
} else {
sub check {
my $self = shift;
- my $recref = $self->hashref;
my $error;
$error=
|| $self->ut_text('svc')
|| $self->ut_alpha('svcdb')
|| $self->ut_enum('disabled', [ '', 'Y' ] )
+ || $self->ut_enum('preserve', [ '', 'Y' ] )
;
return $error if $error;
- my @fields = eval { fields( $recref->{svcdb} ) }; #might die
- return "Unknown svcdb!" unless @fields;
+ my @fields = eval { fields( $self->svcdb ) }; #might die
+ return "Unknown svcdb: ". $self->svcdb. " (Error: $@)"
+ unless @fields;
$self->SUPER::check;
}
my $self = shift;
my %search;
$search{'exporttype'} = shift if @_;
+ sort { $a->weight <=> $b->weight }
map { qsearchs('part_export', { 'exportnum' => $_->exportnum, %search } ) }
qsearch('export_svc', { 'svcpart' => $self->svcpart } );
}
grep $_->can('usage_sessions'), $self->part_export;
}
-=item cust_svc
+=item part_export_did
-Returns a list of associated FS::cust_svc records.
+Returns a list of any exports (see L<FS::part_export>) for this service that
+are capable of returing available DID (phone number) information.
+
+=cut
+
+sub part_export_did {
+ my $self = shift;
+ grep $_->can('get_dids'), $self->part_export;
+}
+
+=item part_export_dsl_pull
+
+Returns a list of any exports (see L<FS::part_export>) for this service that
+are capable of pulling/pushing DSL orders.
+
+=cut
+
+sub part_export_dsl_pull {
+ my $self = shift;
+ grep $_->can('dsl_pull'), $self->part_export;
+}
+
+=item cust_svc [ PKGPART ]
+
+Returns a list of associated customer services (FS::cust_svc records).
+
+If a PKGPART is specified, returns the customer services which are contained
+within packages of that type (see L<FS::part_pkg>). If PKGPARTis specified as
+B<0>, returns unlinked customer services.
=cut
sub cust_svc {
my $self = shift;
- qsearch('cust_svc', { 'svcpart' => $self->svcpart } );
+
+ my $hashref = { 'svcpart' => $self->svcpart };
+
+ my( $addl_from, $extra_sql ) = ( '', '' );
+ if ( @_ ) {
+ my $pkgpart = shift;
+ if ( $pkgpart =~ /^(\d+)$/ ) {
+ $addl_from = 'LEFT JOIN cust_pkg USING ( pkgnum )';
+ $extra_sql = "AND pkgpart = $1";
+ } elsif ( $pkgpart eq '0' ) {
+ $hashref->{'pkgnum'} = '';
+ }
+ }
+
+ qsearch({
+ 'table' => 'cust_svc',
+ 'addl_from' => $addl_from,
+ 'hashref' => $hashref,
+ 'extra_sql' => $extra_sql,
+ });
+}
+
+=item num_cust_svc [ PKGPART ]
+
+Returns the number of associated customer services (FS::cust_svc records).
+
+If a PKGPART is specified, returns the number of customer services which are
+contained within packages of that type (see L<FS::part_pkg>). If PKGPART
+is specified as B<0>, returns the number of unlinked customer services.
+
+=cut
+
+sub num_cust_svc {
+ my $self = shift;
+
+ my @param = ( $self->svcpart );
+
+ my( $join, $and ) = ( '', '' );
+ if ( @_ ) {
+ my $pkgpart = shift;
+ if ( $pkgpart ) {
+ $join = 'LEFT JOIN cust_pkg USING ( pkgnum )';
+ $and = 'AND pkgpart = ?';
+ push @param, $pkgpart;
+ } elsif ( $pkgpart eq '0' ) {
+ $and = 'AND pkgnum IS NULL';
+ }
+ }
+
+ my $sth = dbh->prepare(
+ "SELECT COUNT(*) FROM cust_svc $join WHERE svcpart = ? $and"
+ ) or die dbh->errstr;
+ $sth->execute(@param)
+ or die $sth->errstr;
+ $sth->fetchrow_arrayref->[0];
}
=item svc_x
=back
+=head1 CLASS METHODS
+
+=over 4
+
+=cut
+
+my $svc_defs;
+sub _svc_defs {
+
+ return $svc_defs if $svc_defs; #cache
+
+ my $conf = new FS::Conf;
+
+ #false laziness w/part_pkg.pm::plan_info
+
+ my %info;
+ foreach my $INC ( @INC ) {
+ warn "globbing $INC/FS/svc_*.pm\n" if $DEBUG;
+ foreach my $file ( glob("$INC/FS/svc_*.pm") ) {
+
+ warn "attempting to load service table info from $file\n" if $DEBUG;
+ $file =~ /\/(\w+)\.pm$/ or do {
+ warn "unrecognized file in $INC/FS/: $file\n";
+ next;
+ };
+ my $mod = $1;
+
+ if ( $mod =~ /^svc_[A-Z]/ or $mod =~ /^svc_acct_pop$/ ) {
+ warn "skipping FS::$mod" if $DEBUG;
+ next;
+ }
+
+ eval "use FS::$mod;";
+ if ( $@ ) {
+ die "error using FS::$mod (skipping): $@\n" if $@;
+ next;
+ }
+ unless ( UNIVERSAL::can("FS::$mod", 'table_info') ) {
+ warn "FS::$mod has no table_info method; skipping";
+ next;
+ }
+
+ my $info = "FS::$mod"->table_info;
+ unless ( keys %$info ) {
+ warn "FS::$mod->table_info doesn't return info, skipping\n";
+ next;
+ }
+ warn "got info from FS::$mod: $info\n" if $DEBUG;
+ if ( exists($info->{'disabled'}) && $info->{'disabled'} ) {
+ warn "skipping disabled service FS::$mod" if $DEBUG;
+ next;
+ }
+ $info{$mod} = $info;
+ }
+ }
+
+ tie my %svc_defs, 'Tie::IxHash',
+ map { $_ => $info{$_}->{'fields'} }
+ sort { $info{$a}->{'display_weight'} <=> $info{$b}->{'display_weight'} }
+ keys %info,
+ ;
+
+ # yuck. maybe this won't be so bad when virtual fields become real fields
+ my %vfields;
+ foreach my $svcdb (grep dbdef->table($_), keys %svc_defs ) {
+ eval "use FS::$svcdb;";
+ my $self = "FS::$svcdb"->new;
+ $vfields{$svcdb} = {};
+ foreach my $field ($self->virtual_fields) { # svc_Common::virtual_fields with a null svcpart returns all of them
+ my $pvf = $self->pvf($field);
+ my @list = $pvf->list;
+ if (scalar @list) {
+ $svc_defs{$svcdb}->{$field} = { desc => $pvf->label,
+ type => 'select',
+ select_list => \@list };
+ } else {
+ $svc_defs{$svcdb}->{$field} = $pvf->label;
+ } #endif
+ $vfields{$svcdb}->{$field} = $pvf;
+ warn "\$vfields{$svcdb}->{$field} = $pvf"
+ if $DEBUG;
+ } #next $field
+ } #next $svcdb
+
+ $svc_defs = \%svc_defs; #cache
+
+}
+
+=item svc_tables
+
+Returns a list of all svc_ tables.
+
+=cut
+
+sub svc_tables {
+ my $class = shift;
+ my $svc_defs = $class->_svc_defs;
+ grep { defined( dbdef->table($_) ) } keys %$svc_defs;
+}
+
+=item svc_table_fields TABLE
+
+Given a table name, returns a hashref of field names. The field names
+returned are those with additional (service-definition related) information,
+not necessarily all database fields of the table. Pseudo-fields may also
+be returned (i.e. svc_acct.usergroup).
+
+Each value of the hashref is another hashref, which can have one or more of
+the following keys:
+
+=over 4
+
+=item label - Description of the field
+
+=item def_label - Optional description of the field in the context of service definitions
+
+=item type - Currently "text", "select", "checkbox", "textarea", "disabled",
+some components specified by "select-.*.html", and a bunch more...
+
+=item disable_default - This field should not allow a default value in service definitions
+
+=item disable_fixed - This field should not allow a fixed value in service definitions
+
+=item disable_inventory - This field should not allow inventory values in service definitions
+
+=item select_list - If type is "text", this can be a listref of possible values.
+
+=item select_table - An alternative to select_list, this defines a database table with the possible choices.
+
+=item select_key - Used with select_table, this is the field name of keys
+
+=item select_label - Used with select_table, this is the field name of labels
+
+=back
+
+=cut
+
+#maybe this should move and be a class method in svc_Common.pm
+sub svc_table_fields {
+ my($class, $table) = @_;
+ my $svc_defs = $class->_svc_defs;
+ my $def = $svc_defs->{$table};
+
+ foreach ( grep !ref($def->{$_}), keys %$def ) {
+
+ #normalize the shortcut in %info hash
+ $def->{$_} = { 'label' => $def->{$_} };
+
+ $def->{$_}{'type'} ||= 'text';
+
+ }
+
+ $def;
+}
+
+=back
+
=head1 SUBROUTINES
=over 4
=item process
-Experimental job-queue processor for web interface adds/edits
+Job-queue processor for web interface adds/edits
=cut
my $old = qsearchs('part_svc', { 'svcpart' => $param->{'svcpart'} })
if $param->{'svcpart'};
- $param->{'svc_acct__usergroup'} =
- ref($param->{'svc_acct__usergroup'})
- ? join(',', @{$param->{'svc_acct__usergroup'}} )
- : '';
+ #unmunge cgp_accessmodes (falze laziness-ish w/edit/process/svc_acct.cgi)
+ $param->{'svc_acct__cgp_accessmodes'} ||=
+ join(' ', sort
+ grep { $_ !~ /^(flag|label)$/ }
+ map { /^svc_acct__cgp_accessmodes_([\w\/]+)$/ or die "no way"; $1; }
+ grep $param->{$_},
+ grep /^svc_acct__cgp_accessmodes_([\w\/]+)$/,
+ keys %$param
+ );
+
my $new = new FS::part_svc ( {
map {
$_ => $param->{$_};
} ( fields('part_svc'),
map { my $svcdb = $_;
my @fields = fields($svcdb);
- push @fields, 'usergroup' if $svcdb eq 'svc_acct'; #kludge
- map { ( $svcdb.'__'.$_, $svcdb.'__'.$_.'_flag' ) } @fields;
- } grep defined( dbdef->table($_) ),
- qw( svc_acct svc_domain svc_forward svc_www svc_broadband )
+ push @fields, 'usergroup' if $svcdb eq 'svc_acct'
+ or $svcdb eq 'svc_broadband'; #kludge
+
+ map {
+ my $f = $svcdb.'__'.$_;
+ my $flag = $param->{ $f.'_flag' } || ''; #silence warnings
+ if ( $flag =~ /^[MAH]$/ ) {
+ $param->{ $f } = delete( $param->{ $f.'_classnum' } );
+ }
+ if ( $flag =~ /^S$/
+ or $_ eq 'usergroup' ) {
+ $param->{ $f } = ref($param->{ $f })
+ ? join(',', @{$param->{ $f }} )
+ : $param->{ $f };
+ }
+ ( $f, $f.'_flag', $f.'_label' );
+ }
+ @fields;
+
+ } FS::part_svc->svc_tables()
)
} );
my $error;
if ( $param->{'svcpart'} ) {
$error = $new->replace( $old,
- '1.3-COMPAT',
+ '1.3-COMPAT', #totally bunk, as jeff noted
[ 'usergroup' ],
\%exportnums,
$job
$param->{'svcpart'} = $new->getfield('svcpart');
}
- die $error if $error;
+ die "$error\n" if $error;
+}
+
+=item process_bulk_cust_svc
+
+Job-queue processor for web interface bulk customer service changes
+
+=cut
+
+use Storable qw(thaw);
+use Data::Dumper;
+use MIME::Base64;
+sub process_bulk_cust_svc {
+ my $job = shift;
+
+ my $param = thaw(decode_base64(shift));
+ warn Dumper($param) if $DEBUG;
+
+ local($FS::svc_Common::noexport_hack) = 1
+ if $param->{'noexport'};
+
+ my $old_part_svc =
+ qsearchs('part_svc', { 'svcpart' => $param->{'old_svcpart'} } );
+
+ die "Must select a new service definition\n" unless $param->{'new_svcpart'};
+
+ #the rest should be abstracted out to to its own subroutine?
+
+ 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;
+
+ local( $FS::cust_svc::ignore_quantity ) = 1;
+
+ my $total = $old_part_svc->num_cust_svc( $param->{'pkgpart'} );
+
+ my $n = 0;
+ foreach my $old_cust_svc ( $old_part_svc->cust_svc( $param->{'pkgpart'} ) ) {
+
+ my $new_cust_svc = new FS::cust_svc { $old_cust_svc->hash };
+
+ $new_cust_svc->svcpart( $param->{'new_svcpart'} );
+ my $error = $new_cust_svc->replace($old_cust_svc);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ die "$error\n" if $error;
+ }
+
+ $error = $job->update_statustext( int( 100 * ++$n / $total ) );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ die $error if $error;
+ }
+
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ '';
+
+}
+
+sub _upgrade_data { #class method
+ my ($class, %opts) = @_;
+
+ my @part_svc_column = qsearch('part_svc_column', { 'columnname' => 'usergroup' });
+ foreach my $col ( @part_svc_column ) {
+ next if $col->columnvalue =~ /^[\d,]+$/ || !$col->columnvalue;
+ my @groupnames = split(',',$col->columnvalue);
+ my @groupnums;
+ my $error = '';
+ foreach my $groupname ( @groupnames ) {
+ my $g = qsearchs('radius_group', { 'groupname' => $groupname } );
+ unless ( $g ) {
+ $g = new FS::radius_group {
+ 'groupname' => $groupname,
+ 'description' => $groupname,
+ };
+ $error = $g->insert;
+ die $error if $error;
+ }
+ push @groupnums, $g->groupnum;
+ }
+ $col->columnvalue(join(',',@groupnums));
+ $error = $col->replace;
+ die $error if $error;
+ }
+
}
=head1 BUGS
Delete is unimplemented.
-The list of svc_* tables is hardcoded. When svc_acct_pop is renamed, this
-should be fixed.
+The list of svc_* tables is no longer hardcoded, but svc_acct_pop is skipped
+as a special case until it is renamed.
-all_part_svc_column method should be documented
+all_part_svc_column methods should be documented
=head1 SEE ALSO