Checks the validity of fields in this record.
-At present, this does nothing but call FS::Record::check (which, in turn,
-does nothing but run virtual field checks).
+Only checks fields marked as required in table_info or
+part_svc_column definition. Should be invoked by service-specific
+check using SUPER. Invokes FS::Record::check using SUPER.
=cut
sub check {
my $self = shift;
+
+ ## Checking required fields
+
+ # get fields marked as required in table_info
+ my $required = {};
+ my $labels = {};
+ my $tinfo = $self->can('table_info') ? $self->table_info : {};
+ if ($tinfo->{'manual_require'}) {
+ my $fields = $tinfo->{'fields'} || {};
+ foreach my $field (keys %$fields) {
+ if (ref($fields->{$field}) && $fields->{$field}->{'required'}) {
+ $required->{$field} = 1;
+ $labels->{$field} = $fields->{$field}->{'label'};
+ }
+ }
+ # add fields marked as required in database
+ foreach my $column (
+ qsearch('part_svc_column',{
+ 'svcpart' => $self->svcpart,
+ 'required' => 'Y'
+ })
+ ) {
+ $required->{$column->columnname} = 1;
+ $labels->{$column->columnname} = $column->columnlabel;
+ }
+ # do the actual checking
+ foreach my $field (keys %$required) {
+ unless (length($self->get($field)) > 0) {
+ my $name = $labels->{$field} || $field;
+ return "$name is required\n"
+ }
+ }
+ }
+
$self->SUPER::check;
}
$self->svcpart($cust_svc->svcpart);
}
- my $error = $self->preinsert_hook_first
+ my $error = $self->preinsert_hook_first(%options)
|| $self->set_auto_inventory
|| $self->check
|| $self->_check_duplicate
sub _check_duplcate { ''; }
sub preinsert_hook { ''; }
sub table_dupcheck_fields { (); }
+sub prereplace_hook { ''; }
+sub prereplace_hook_first { ''; }
sub predelete_hook { ''; }
sub predelete_hook_first { ''; }
|| $self->SUPER::delete
|| $self->export('delete', @$export_args)
|| $self->return_inventory
+ || $self->release_router
|| $self->predelete_hook
|| $self->cust_svc->delete
;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- my $error = $new->set_auto_inventory($old);
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
-
- #redundant, but so any duplicate fields are maniuplated as appropriate
- # (svc_phone.phonenum)
- $error = $new->check;
+ my $error = $new->prereplace_hook_first($old)
+ || $new->set_auto_inventory($old)
+ || $new->check; #redundant, but so any duplicate fields are
+ #maniuplated as appropriate (svc_phone.phonenum)
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
=cut
sub set_auto_inventory {
+ # don't try to do this during an upgrade
+ return '' if $FS::CurrentUser::upgrade_hack;
+
my $self = shift;
my $old = @_ ? shift : '';
next if $columnflag eq 'A' && $self->$field() ne '';
my $classnum = $part_svc_column->columnvalue;
- my %hash = ( 'classnum' => $classnum );
+ my %hash;
if ( $columnflag eq 'A' && $self->$field() eq '' ) {
$hash{'svcnum'} = '';
} elsif ( $columnflag eq 'M' ) {
return "Select inventory item for $field" unless $self->getfield($field);
$hash{'item'} = $self->getfield($field);
+ my $chosen_classnum = $self->getfield($field.'_classnum');
+ if ( grep {$_ == $chosen_classnum} split(',', $classnum) ) {
+ $classnum = $chosen_classnum;
+ }
+ # otherwise the chosen classnum is either (all), or somehow not on
+ # the list, so ignore it and choose the first item that's in any
+ # class on the list
}
my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql(
my $inventory_item = qsearchs({
'table' => 'inventory_item',
'hashref' => \%hash,
- 'extra_sql' => "AND $agentnums_sql",
+ 'extra_sql' => "AND classnum IN ($classnum) AND $agentnums_sql",
'order_by' => 'ORDER BY ( agentnum IS NULL ) '. #agent inventory first
' LIMIT 1 FOR UPDATE',
});
unless ( $inventory_item ) {
+ # should really only be shown if columnflag eq 'A'...
$dbh->rollback if $oldAutoCommit;
- my $inventory_class =
- qsearchs('inventory_class', { 'classnum' => $classnum } );
- return "Can't find inventory_class.classnum $classnum"
- unless $inventory_class;
- return "Out of ". PL_N($inventory_class->classname);
+ my $message = 'Out of ';
+ my @classnums = split(',', $classnum);
+ foreach ( @classnums ) {
+ my $class = FS::inventory_class->by_key($_)
+ or return "Can't find inventory_class.classnum $_";
+ $message .= PL_N($class->classname);
+ if ( scalar(@classnums) > 2 ) { # english is hard
+ if ( $_ != $classnums[-1] ) {
+ $message .= ', ';
+ }
+ }
+ if ( scalar(@classnums) > 1 and $_ == $classnums[-2] ) {
+ $message .= 'and ';
+ }
+ }
+ return $message;
}
next if $columnflag eq 'M' && $inventory_item->svcnum == $self->svcnum;
$self->setfield( $field, $inventory_item->item );
#if $columnflag eq 'A' && $self->$field() eq '';
+ # release the old inventory item, if there was one
if ( $old && $old->$field() && $old->$field() ne $self->$field() ) {
my $old_inv = qsearchs({
'table' => 'inventory_item',
- 'hashref' => { 'classnum' => $classnum,
+ 'hashref' => {
'svcnum' => $old->svcnum,
},
- 'extra_sql' => ' AND '.
+ 'extra_sql' => "AND classnum IN ($classnum) AND ".
'( ( svc_field IS NOT NULL AND svc_field = '.$dbh->quote($field).' )'.
' OR ( svc_field IS NULL AND item = '. dbh->quote($old->$field).' )'.
')',
=item return_inventory
+Release all inventory items attached to this service's fields. Call
+when unprovisioning the service.
+
=cut
sub return_inventory {
});
}
+=item release_router
+
+Delete any routers associated with this service. This will release their
+address blocks, also.
+
+=cut
+
+sub release_router {
+ my $self = shift;
+ my @routers = qsearch('router', { svcnum => $self->svcnum });
+ foreach (@routers) {
+ my $error = $_->delete;
+ return "$error (removing router '".$_->routername."')" if $error;
+ }
+ '';
+}
+
+
=item cust_svc
Returns the cust_svc record associated with this svc_ record, as a FS::cust_svc
sub export {
my( $self, $method ) = ( shift, shift );
+ # $method must start with export_, $action must be the part after that
$method = "export_$method" unless $method =~ /^export_/;
+ my ($action) = $method =~ /^export_(\w+)/;
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
unless ( $noexport_hack ) {
foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
next unless $part_export->can($method);
+ next if $part_export->get("no_$action"); # currently only 'no_suspend'
my $error = $part_export->$method($self, @_);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
#my( $class, $string ) = @_;
'1 = 0'; #false
}
+sub search_sql_addl_from {
+ '';
+}
=item search HASHREF
=item order_by
+=item cancelled - if true, only returns svcs attached to cancelled pkgs;
+if defined and false, only returns svcs not attached to cancelled packages
+
=back
=cut
-# based on FS::svc_acct::search, both that and svc_broadband::search should
-# eventually use this instead
+### Don't call the 'cancelled' option 'Service Status'
+### There is no such thing
+### See cautionary note in httemplate/browse/part_svc.cgi
+
sub search {
my ($class, $params) = @_;
'LEFT JOIN cust_svc USING ( svcnum )',
'LEFT JOIN part_svc USING ( svcpart )',
'LEFT JOIN cust_pkg USING ( pkgnum )',
- 'LEFT JOIN cust_main USING ( custnum )',
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'),
);
my @where = ();
+ $class->_search_svc($params, \@from, \@where) if $class->can('_search_svc');
+
# # domain
# if ( $params->{'domain'} ) {
# my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
#custnum
if ( $params->{'custnum'} =~ /^(\d+)$/ && $1 ) {
- push @where, "custnum = $1";
+ push @where, "cust_pkg.custnum = $1";
}
#customer status
}
#pkgpart
- if ( $params->{'pkgpart'} && scalar(@{ $params->{'pkgpart'} }) ) {
- my @pkgpart = grep /^(\d+)$/, @{ $params->{'pkgpart'} };
- push @where, 'cust_pkg.pkgpart IN ('. join(',', @pkgpart ). ')';
+ ##pkgpart, now properly untainted, can be arrayref
+ #for my $pkgpart ( $params->{'pkgpart'} ) {
+ # if ( ref $pkgpart ) {
+ # my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
+ # push @where, "cust_pkg.pkgpart IN ($where)" if $where;
+ # }
+ # elsif ( $pkgpart =~ /^(\d+)$/ ) {
+ # push @where, "cust_pkg.pkgpart = $1";
+ # }
+ #}
+ if ( $params->{'pkgpart'} ) {
+ my @pkgpart = ref( $params->{'pkgpart'} )
+ ? @{ $params->{'pkgpart'} }
+ : $params->{'pkgpart'}
+ ? ( $params->{'pkgpart'} )
+ : ();
+ @pkgpart = grep /^(\d+)$/, @pkgpart;
+ push @where, 'cust_pkg.pkgpart IN ('. join(',', @pkgpart ). ')' if @pkgpart;
+ }
+
+ #svcnum
+ if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
+ push @where, "svcnum = $1";
}
# svcpart
- if ( $params->{'svcpart'} && scalar(@{ $params->{'svcpart'} }) ) {
- my @svcpart = grep /^(\d+)$/, @{ $params->{'svcpart'} };
- push @where, 'svcpart IN ('. join(',', @svcpart ). ')';
+ if ( $params->{'svcpart'} ) {
+ my @svcpart = ref( $params->{'svcpart'} )
+ ? @{ $params->{'svcpart'} }
+ : $params->{'svcpart'}
+ ? ( $params->{'svcpart'} )
+ : ();
+ @svcpart = grep /^(\d+)$/, @svcpart;
+ push @where, 'svcpart IN ('. join(',', @svcpart ). ')' if @svcpart;
}
if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
push @where, "exportnum = $1";
}
+ if ( defined($params->{'cancelled'}) ) {
+ if ($params->{'cancelled'}) {
+ push @where, "cust_pkg.cancel IS NOT NULL";
+ } else {
+ push @where, "cust_pkg.cancel IS NULL";
+ }
+ }
+
# # sector and tower
# my @where_sector = $class->tower_sector_sql($params);
# if ( @where_sector ) {