X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fpart_svc.pm;h=dcc78435b0afac3ba635797bbd496dd4ab1ce77d;hp=7e592bf7228135c66eab2f995faf4666828abf6c;hb=f413badbfe4676563d11b528838a21d9ceb8da14;hpb=fb4ab1073f0d15d660c6cdc4e07afebf68ef3924 diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm index 7e592bf72..dcc78435b 100644 --- a/FS/FS/part_svc.pm +++ b/FS/FS/part_svc.pm @@ -1,7 +1,8 @@ package FS::part_svc; +use base qw(FS::o2m_Common FS::Record); use strict; -use vars qw( @ISA $DEBUG ); +use vars qw( $DEBUG ); use Tie::IxHash; use FS::Record qw( qsearch qsearchs fields dbh ); use FS::Schema qw( dbdef ); @@ -9,8 +10,13 @@ use FS::part_svc_column; use FS::part_export; use FS::export_svc; use FS::cust_svc; +use FS::part_svc_class; +use FS::part_svc_msgcat; -@ISA = qw(FS::Record); +FS::UID->install_callback(sub { + # preload the cache and make sure all modules load + my $svc_defs = FS::part_svc->_svc_defs; +}); $DEBUG = 0; @@ -51,10 +57,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 @@ -82,8 +101,12 @@ the part_svc_column table appropriately (see L). =item I__I - Default or fixed value for I in I. +=item I__I_label + =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. +=item I__I_required - I should always have a true value + =back If you want to add part_svc_column records for fields that do not exist as @@ -100,12 +123,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 @_; @@ -129,61 +148,64 @@ sub insert { # add part_svc_column records my $svcdb = $self->svcdb; -# my @rows = map { /^${svcdb}__(.*)$/; $1 } -# grep ! /_flag$/, -# grep /^${svcdb}__/, -# fields('part_svc'); - foreach my $field ( - grep { $_ ne 'svcnum' - && ( defined( $self->getfield($svcdb.'__'.$_.'_flag') ) - || $self->getfield($svcdb.'__'.$_.'_label') !~ /^\s*$/ ) - } (fields($svcdb), @fields) - ) { - my $part_svc_column = $self->part_svc_column($field); - my $previous = qsearchs('part_svc_column', { - 'svcpart' => $self->svcpart, - 'columnname' => $field, - } ); + foreach my $field (fields($svcdb), @fields) { + next if $field eq 'svcnum'; + my $prefix = $svcdb.'__'; + if ( defined( $self->getfield($prefix.$field.'_flag')) + or defined($self->getfield($prefix.$field.'_required')) + or length($self->getfield($prefix.$field.'_label')) + ) { + my $part_svc_column = $self->part_svc_column($field); + my $previous = qsearchs('part_svc_column', { + 'svcpart' => $self->svcpart, + 'columnname' => $field, + } ); - my $flag = $self->getfield($svcdb.'__'.$field.'_flag'); - my $label = $self->getfield($svcdb.'__'.$field.'_label'); - if ( uc($flag) =~ /^([A-Z])$/ || $label !~ /^\s*$/ ) { + my $flag = $self->getfield($prefix.$field.'_flag'); + my $label = $self->getfield($prefix.$field.'_label'); + my $required = $self->getfield($prefix.$field.'_required') ? 'Y' : ''; + 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)) - ); - } + 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($prefix.$field)) + ); + } - $part_svc_column->setfield('columnlabel', $label) - if $label !~ /^\s*$/; + $part_svc_column->setfield('columnlabel', $label) + if $label !~ /^\s*$/; + + $part_svc_column->setfield('required', $required); + + if ( $previous ) { + $error = $part_svc_column->replace($previous); + } else { + $error = $part_svc_column->insert; + } - if ( $previous ) { - $error = $part_svc_column->replace($previous); } else { - $error = $part_svc_column->insert; + $error = $previous ? $previous->delete : ''; + } + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; } - } else { - $error = $previous ? $previous->delete : ''; } - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - } # 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 ) { @@ -265,58 +287,63 @@ sub replace { # maintain part_svc_column records my $svcdb = $new->svcdb; - foreach my $field ( - grep { $_ ne 'svcnum' - && ( 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'); - my $label = $new->getfield($svcdb.'__'.$field.'_label'); + foreach my $field (fields($svcdb),@fields) { + next if $field eq 'svcnum'; + my $prefix = $svcdb.'__'; + if ( defined( $new->getfield($prefix.$field.'_flag')) + or defined($new->getfield($prefix.$field.'_required')) + or length($new->getfield($prefix.$field.'_label')) + ) { + 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'); + my $label = $new->getfield($svcdb.'__'.$field.'_label'); + my $required = $new->getfield($svcdb.'__'.$field.'_required') ? 'Y' : ''; - if ( uc($flag) =~ /^([A-Z])$/ || $label !~ /^\s*$/ ) { + 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', ''); + } - 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*$/; - $part_svc_column->setfield('columnlabel', $label) - if $label !~ /^\s*$/; + $part_svc_column->setfield('required', $required); - if ( $previous ) { - $error = $part_svc_column->replace($previous); + if ( $previous ) { + $error = $part_svc_column->replace($previous); + } else { + $error = $part_svc_column->insert; + } } else { - $error = $part_svc_column->insert; + $error = $previous ? $previous->delete : ''; + } + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; } - } else { - $error = $previous ? $previous->delete : ''; - } - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; } } # 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; @@ -326,13 +353,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 ); } @@ -384,10 +421,13 @@ sub check { $self->ut_numbern('svcpart') || $self->ut_text('svc') || $self->ut_alpha('svcdb') - || $self->ut_enum('disabled', [ '', 'Y' ] ) - || $self->ut_enum('preserve', [ '', '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 @@ -437,9 +477,10 @@ sub part_export { 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 } ); + 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 @@ -463,7 +504,7 @@ are capable of returing available DID (phone number) information. sub part_export_did { my $self = shift; - grep $_->can('get_dids'), $self->part_export; + grep $_->can_get_dids, $self->part_export; } =item part_export_dsl_pull @@ -525,6 +566,9 @@ is specified as B<0>, returns the number of unlinked customer services. sub num_cust_svc { my $self = shift; + return $self->{Hash}{num_cust_svc} + if !@_ && exists($self->{Hash}{num_cust_svc}); + my @param = ( $self->svcpart ); my( $join, $and ) = ( '', '' ); @@ -547,6 +591,26 @@ sub num_cust_svc { $sth->fetchrow_arrayref->[0]; } +=item num_cust_svc_cancelled + +Returns the number of associated customer services that are +attached to cancelled packages. + +=cut + +sub num_cust_svc_cancelled { + my $self = shift; + my $sth = dbh->prepare( + "SELECT COUNT(*) FROM cust_svc + LEFT JOIN cust_pkg USING ( pkgnum ) + WHERE svcpart = ? + AND cust_pkg.cancel IS NOT NULL" + ) or die dbh->errstr; + $sth->execute($self->svcpart) + or die $sth->errstr; + $sth->fetchrow_arrayref->[0]; +} + =item svc_x Returns a list of associated FS::svc_* records. @@ -558,6 +622,24 @@ sub svc_x { map { $_->svc_x } $self->cust_svc; } +=item svc_locale LOCALE + +Returns a customer-viewable service definition label in the chosen LOCALE. +If there is no entry for that locale or if LOCALE is empty, returns +part_svc.svc. + +=cut + +sub svc_locale { + my( $self, $locale ) = @_; + return $self->svc unless $locale; + my $part_svc_msgcat = qsearchs('part_svc_msgcat', { + svcpart => $self->svcpart, + locale => $locale + }) or return $self->svc; + $part_svc_msgcat->svc; +} + =back =head1 CLASS METHODS @@ -567,6 +649,7 @@ sub svc_x { =cut my $svc_defs; +my $svc_info; sub _svc_defs { return $svc_defs if $svc_defs; #cache @@ -587,7 +670,7 @@ sub _svc_defs { }; my $mod = $1; - if ( $mod =~ /^svc_[A-Z]/ or $mod =~ /^svc_acct_pop$/ ) { + if ( $mod =~ /^svc_[A-Z]/ or $mod =~ /^(svc_acct_pop|svc_export_machine)$/ ) { warn "skipping FS::$mod" if $DEBUG; next; } @@ -613,15 +696,30 @@ sub _svc_defs { next; } $info{$mod} = $info; + + # all svc_* modules are required to have h_svc_* modules for invoice + # display. check for them as early as possible. + eval "use FS::h_$mod;"; + if ( $@ ) { + die "couldn't load history record module h_$mod: $@\n"; + } } } + tie my %svc_defs, 'Tie::IxHash', map { $_ => $info{$_}->{'fields'} } sort { $info{$a}->{'display_weight'} <=> $info{$b}->{'display_weight'} } keys %info, ; - + + tie my %svc_info, 'Tie::IxHash', + map { $_ => $info{$_} } + sort { $info{$a}->{'display_weight'} <=> $info{$b}->{'display_weight'} } + keys %info, + ; + + $svc_info = \%svc_info; #access via svc_table_info $svc_defs = \%svc_defs; #cache } @@ -671,6 +769,10 @@ some components specified by "select-.*.html", and a bunch more... =item select_label - Used with select_table, this is the field name of labels +=item select_allow_empty - Used with select_table, adds an empty option + +=item required - This field should always have a true value (do not use with type checkbox or disabled) + =back =cut @@ -693,6 +795,27 @@ sub svc_table_fields { $def; } +=item svc_table_info TABLE + +Returns table_info for TABLE from cache, or empty +hashref if none is found. + +Caution: caches table_info for ALL services when run; +access a service's table_info directly unless you know +you're loading them all. + +Caution: does not standardize fields into hashrefs; +use L to access fields. + +=cut + +sub svc_table_info { + my $class = shift; + my $table = shift; + $class->_svc_defs; #creates cache if needed + return $svc_info->{$table} || {}; +} + =back =head1 SUBROUTINES @@ -705,13 +828,10 @@ Job-queue processor for web interface adds/edits =cut -use Storable qw(thaw); use Data::Dumper; -use MIME::Base64; sub process { my $job = shift; - - my $param = thaw(decode_base64(shift)); + my $param = shift; warn Dumper($param) if $DEBUG; my $old = qsearchs('part_svc', { 'svcpart' => $param->{'svcpart'} }) @@ -744,13 +864,11 @@ sub process { 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 }; + if ( ( $flag =~ /^[MAHS]$/ or $_ eq 'usergroup' ) + and ref($param->{ $f }) ) { + $param->{ $f } = join(',', @{ $param->{ $f } }); } - ( $f, $f.'_flag', $f.'_label' ); + ( $f, $f.'_flag', $f.'_label', $f.'_required' ); } @fields; @@ -761,7 +879,13 @@ sub process { 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, @@ -778,6 +902,12 @@ sub process { $param->{'svcpart'} = $new->getfield('svcpart'); } + $error ||= $new->process_o2m( + 'table' => 'part_svc_msgcat', + 'params' => $param, + 'fields' => [ 'locale', 'svc' ], + ); + die "$error\n" if $error; } @@ -787,13 +917,10 @@ 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)); + my $param = shift; warn Dumper($param) if $DEBUG; local($FS::svc_Common::noexport_hack) = 1