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;
use FS::part_export;
use FS::export_svc;
use FS::cust_svc;
+use FS::part_svc_class;
@ISA = qw(FS::Record);
-$DEBUG = 1;
+$DEBUG = 0;
=head1 NAME
=item svcdb - table used for this service. See L<FS::svc_acct>,
L<FS::svc_domain>, and L<FS::svc_forward>, among others.
+=item classnum - Optional service class (see L<FS::svc_class>)
+
=item disabled - Disabled flag, empty or `Y'
+=item preserve - Preserve after cancellation, empty or 'Y'
+
+=item selfservice_access - Access allowed to the service via self-service:
+empty for full access, "readonly" for read-only, "hidden" to hide it entirely
+
+=item restrict_edit_password - Require the "Provision customer service" access
+right to change the password field, rather than just "Edit password". Only
+relevant to svc_acct for now.
+
+=item has_router - Allow the service to have an L<FS::router> connected
+through it. Probably only relevant to svc_broadband, svc_acct, and svc_dsl
+for now.
+
=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 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.
+=item I<svcdb>__I<field>_flag - defines I<svcdb>__I<field> action: null or empty (no default), `D' for default, `F' for fixed (unchangeable), , `S' for selectable choice, `M' for manual selection from inventory, or `A' for automatic selection from inventory. For virtual fields, can also be 'X' for excluded.
=back
If you want to add part_svc_column records for fields that do not exist as
-(real or virtual) fields in the I<svcdb> table, make sure to list then in
+fields in the I<svcdb> table, make sure to list then in
EXTRA_FIELDS_ARRAYREF also.
If EXPORTNUMS_HASHREF is specified (keys are exportnums and values are
# 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) =~ /^([DFMAX])$/ ) {
- if ( uc($flag) =~ /^([A-Z])$/ ) { #part_svc_column will test it
- $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) =~ /^([DFMAX])$/ ) {
- if ( uc($flag) =~ /^([A-Z])$/ ) { #part_svc_column will test it
- $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 {
$self->ut_numbern('svcpart')
|| $self->ut_text('svc')
|| $self->ut_alpha('svcdb')
- || $self->ut_enum('disabled', [ '', 'Y' ] )
- ;
+ || $self->ut_flag('disabled')
+ || $self->ut_flag('preserve')
+ || $self->ut_enum('selfservice_access', [ '', 'hidden', 'readonly' ] )
+ || $self->ut_foreign_keyn('classnum', 'part_svc_class', 'classnum' )
+ || $self->ut_flag('restrict_edit_password')
+ || $self->ut_flag('has_router')
+;
return $error if $error;
my @fields = eval { fields( $self->svcdb ) }; #might die
my $self = shift;
my %search;
$search{'exporttype'} = shift if @_;
- map { qsearchs('part_export', { 'exportnum' => $_->exportnum, %search } ) }
- qsearch('export_svc', { 'svcpart' => $self->svcpart } );
+ map { $_ } #behavior of sort undefined in scalar context
+ sort { $a->weight <=> $b->weight }
+ map { qsearchs('part_export', { 'exportnum'=>$_->exportnum, %search } ) }
+ qsearch('export_svc', { 'svcpart'=>$self->svcpart } );
}
=item part_export_usage
grep $_->can('usage_sessions'), $self->part_export;
}
+=item part_export_did
+
+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).
map { $_->svc_x } $self->cust_svc;
}
+=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|svc_export_machine)$/ ) {
+ 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,
+ ;
+
+ $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
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'}} )
- : $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
+ push @fields, 'usergroup' if $svcdb eq 'svc_acct'
+ or $svcdb eq 'svc_broadband'; #kludge
map {
- if ( $param->{ $svcdb.'__'.$_.'_flag' } =~ /^[MA]$/ ) {
- $param->{ $svcdb.'__'.$_ } =
- delete( $param->{ $svcdb.'__'.$_.'_classnum' } );
+ my $f = $svcdb.'__'.$_;
+ my $flag = $param->{ $f.'_flag' } || ''; #silence warnings
+ if ( $flag =~ /^[MAH]$/ ) {
+ $param->{ $f } = delete( $param->{ $f.'_classnum' } );
}
- ( $svcdb.'__'.$_, $svcdb.'__'.$_.'_flag' );
+ if ( ( $flag =~ /^[MAHS]$/ or $_ eq 'usergroup' )
+ and ref($param->{ $f }) ) {
+ $param->{ $f } = join(',', @{ $param->{ $f } });
+ }
+ ( $f, $f.'_flag', $f.'_label' );
}
@fields;
- } grep defined( dbdef->table($_) ),
- qw( svc_acct svc_domain svc_forward svc_www svc_broadband
- svc_phone svc_external
- )
+ } 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
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'} } );
}
+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 inserting new radius_group for service definition group \"$groupname\": $error"
+ if $error;
+ }
+ push @groupnums, $g->groupnum;
+ }
+ $col->columnvalue(join(',',@groupnums));
+ $error = $col->replace;
+ die $error if $error;
+ }
+
+ my @badlabels = qsearch({
+ 'table' => 'part_svc_column',
+ 'hashref' => {},
+ 'extra_sql' => 'WHERE columnlabel IN ('.
+ "'Descriptive label for this particular device.',".
+ "'IP address. Leave blank for automatic assignment.',".
+ "'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',".
+ "'Maximum download speed for this service in Kbps. 0 denotes unlimited.')"
+ });
+ foreach my $col ( @badlabels ) {
+ $col->columnlabel('');
+ my $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 methods should be documented