X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fpart_svc.pm;h=0bef7bcf5f23ecd1201326e5ab5f41aafb80f91c;hp=258734735627fec9fbdd39b4f9545aa2470a7dee;hb=7e6ab4562bb5490ca82422d91b6389fd844ea6ff;hpb=1053db7f76169cbbc87840539959a4c362aff242 diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm index 258734735..0bef7bcf5 100644 --- a/FS/FS/part_svc.pm +++ b/FS/FS/part_svc.pm @@ -2,16 +2,18 @@ package FS::part_svc; 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 @@ -50,8 +52,23 @@ FS::Record. The following fields are currently supported: =item svcdb - table used for this service. See L, L, and L, among others. +=item classnum - Optional service class (see L) + =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 connected +through it. Probably only relevant to svc_broadband, svc_acct, and svc_dsl +for now. + =back =head1 METHODS @@ -79,12 +96,12 @@ the part_svc_column table appropriately (see L). =item I__I - Default or fixed value for I in I. -=item I__I_flag - defines I__I 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__I_flag - defines I__I 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 table, make sure to list then in +fields in the I table, make sure to list then in EXTRA_FIELDS_ARRAYREF also. If EXPORTNUMS_HASHREF is specified (keys are exportnums and values are @@ -97,12 +114,8 @@ TODOC: JOB sub insert { my $self = shift; my @fields = (); - my @exportnums = (); @fields = @{shift(@_)} if @_; - if ( @_ ) { - my $exportnums = shift; - @exportnums = grep $exportnums->{$_}, keys %$exportnums; - } + my $exportnums = shift || {}; my $job = ''; $job = shift if @_; @@ -132,7 +145,8 @@ sub insert { # 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); @@ -141,18 +155,28 @@ sub insert { '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 : ''; } @@ -164,12 +188,14 @@ sub insert { } # add export_svc records + my @exportnums = grep $exportnums->{$_}, keys %$exportnums; my $slice = 100/scalar(@exportnums) if @exportnums; my $done = 0; foreach my $exportnum ( @exportnums ) { my $export_svc = new FS::export_svc ( { 'exportnum' => $exportnum, 'svcpart' => $self->svcpart, + 'role' => $exportnums->{$exportnum}, } ); $error = $export_svc->insert($job, $slice*$done++, $slice); if ( $error ) { @@ -178,6 +204,8 @@ sub insert { } } + # XXX shouldn't this update fixed values? + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; @@ -251,22 +279,37 @@ sub replace { 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 { @@ -283,9 +326,10 @@ sub replace { # maintain export_svc records - if ( $exportnums ) { + if ( $exportnums ) { # hash of exportnum => role #false laziness w/ edit/process/agent_type.cgi + #and, more importantly, with m2m_Common my @new_export_svc = (); foreach my $part_export ( qsearch('part_export', {}) ) { my $exportnum = $part_export->exportnum; @@ -295,13 +339,23 @@ sub replace { }; my $export_svc = qsearchs('export_svc', $hashref); - if ( $export_svc && ! $exportnums->{$exportnum} ) { - $error = $export_svc->delete; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; + if ( $export_svc ) { + my $old_role = $export_svc->role || 1; # 1 = null in the db + if ( ! $exportnums->{$exportnum} + or $old_role ne $exportnums->{$exportnum} ) { + + $error = $export_svc->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + undef $export_svc; # on a role change, force it to be reinserted + } - } elsif ( ! $export_svc && $exportnums->{$exportnum} ) { + } # if $export_svc + if ( ! $export_svc && $exportnums->{$exportnum} ) { + # also applies if it's been undef'd because of role change + $hashref->{role} = $exportnums->{$exportnum}; push @new_export_svc, new FS::export_svc ( $hashref ); } @@ -353,8 +407,13 @@ sub check { $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 @@ -404,8 +463,10 @@ sub part_export { 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 @@ -420,6 +481,30 @@ sub part_export_usage { grep $_->can('usage_sessions'), $self->part_export; } +=item part_export_did + +Returns a list of any exports (see L) 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) 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). @@ -500,6 +585,140 @@ sub svc_x { 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 @@ -525,11 +744,17 @@ sub process { 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->{$_}; @@ -537,32 +762,41 @@ sub process { } ( 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 %exportnums = map { $_->exportnum => ( $param->{'exportnum'.$_->exportnum} || '') } qsearch('part_export', {} ); - + foreach my $exportnum (%exportnums) { + my $role = $param->{'exportnum'.$exportnum.'_role'}; + # role is undef if the export has no role selector + if ( $exportnums{$exportnum} && $role ) { + $exportnums{$exportnum} = $role; + } + } my $error; if ( $param->{'svcpart'} ) { $error = $new->replace( $old, - '1.3-COMPAT', + '1.3-COMPAT', #totally bunk, as jeff noted [ 'usergroup' ], \%exportnums, $job @@ -593,6 +827,9 @@ sub process_bulk_cust_svc { 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'} } ); @@ -641,12 +878,56 @@ sub process_bulk_cust_svc { } +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