package FS::part_svc;
+use base qw(FS::o2m_Common FS::Record);
use strict;
use vars qw( @ISA $DEBUG );
use Tie::IxHash;
use FS::export_svc;
use FS::cust_svc;
use FS::part_svc_class;
-
-@ISA = qw(FS::Record);
+use FS::part_svc_msgcat;
$DEBUG = 0;
=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>_label
+
=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.
+=item I<svcdb>__I<field>_required - I<field> should always have a true value
+
=back
If you want to add part_svc_column records for fields that do not exist as
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 @_;
# 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 ) {
# 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;
};
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 );
}
$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
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
grep $_->can('dsl_pull'), $self->part_export;
}
+=item part_export_partsvc
+
+Returns a list of any exports (see L<FS::part_export>) for this service that
+are capable of pushing a change after part svc is changed.
+
+=cut
+
+sub part_export_partsvc {
+ my $self = shift;
+ grep $_->can('export_partsvc'), $self->part_export;
+}
+
=item cust_svc [ PKGPART ]
Returns a list of associated customer services (FS::cust_svc records).
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 ) = ( '', '' );
$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.
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;
+}
+
+# 3.x stub
+sub part_svc_msgcat {
+ my $self = shift;
+ qsearch('part_svc_msgcat', { 'svcpart' => $self->svcpart });
+}
+
=back
=head1 CLASS METHODS
=cut
my $svc_defs;
+my $svc_info;
sub _svc_defs {
return $svc_defs if $svc_defs; #cache
};
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;
}
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
}
=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
$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</svc_table_fields> 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
map {
my $f = $svcdb.'__'.$_;
my $flag = $param->{ $f.'_flag' } || ''; #silence warnings
- if ( $flag =~ /^[MAH]$/ ) {
+ if ( $flag =~ /^[MAHP]$/ ) {
$param->{ $f } = delete( $param->{ $f.'_classnum' } );
}
- if ( $flag =~ /^S$/
- or $_ eq 'usergroup' ) {
- $param->{ $f } = ref($param->{ $f })
- ? join(',', @{$param->{ $f }} )
- : $param->{ $f };
+ if ( ( $flag =~ /^[MAHSP]$/ 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;
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,
$param->{'svcpart'} = $new->getfield('svcpart');
}
+ $error ||= $new->process_o2m(
+ 'table' => 'part_svc_msgcat',
+ 'params' => $param,
+ 'fields' => [ 'locale', 'svc' ],
+ );
+
die "$error\n" if $error;
+
+ foreach my $part_svc_export ( $new->part_export_partsvc ) {
+ $error = $part_svc_export->export_partsvc($new);
+ }
+ return $error if $error;
}
=item process_bulk_cust_svc