summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
authorivan <ivan>2006-12-29 08:51:34 +0000
committerivan <ivan>2006-12-29 08:51:34 +0000
commit633c48448d9468690b7ad77eb6ff7c660a286658 (patch)
tree4e08051f8d805e7e95c8dffe5e3e73b0c360f965 /FS
parent6cb5c702b17b98be46adea4539e15d5f312e5be1 (diff)
service refactor!
Diffstat (limited to 'FS')
-rw-r--r--FS/FS/Record.pm34
-rw-r--r--FS/FS/Schema.pm27
-rw-r--r--FS/FS/UI/Web.pm111
-rw-r--r--FS/FS/cust_main.pm61
-rw-r--r--FS/FS/cust_pkg.pm127
-rw-r--r--FS/FS/cust_svc.pm65
-rw-r--r--FS/FS/part_svc.pm167
-rw-r--r--FS/FS/pkg_svc.pm4
-rw-r--r--FS/FS/registrar.pm119
-rw-r--r--FS/FS/svc_Common.pm127
-rw-r--r--FS/FS/svc_External_Common.pm199
-rw-r--r--FS/FS/svc_Parent_Mixin.pm103
-rw-r--r--FS/FS/svc_acct.pm129
-rwxr-xr-xFS/FS/svc_broadband.pm42
-rw-r--r--FS/FS/svc_domain.pm99
-rw-r--r--FS/FS/svc_external.pm70
-rw-r--r--FS/FS/svc_forward.pm59
-rw-r--r--FS/FS/svc_phone.pm42
-rw-r--r--FS/FS/svc_www.pm25
-rw-r--r--FS/MANIFEST6
-rw-r--r--FS/t/registrar.t5
-rw-r--r--FS/t/svc_External_Common.t5
-rw-r--r--FS/t/svc_Parent_Mixin.t5
23 files changed, 1381 insertions, 250 deletions
diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm
index c25b9be..ba03091 100644
--- a/FS/FS/Record.pm
+++ b/FS/FS/Record.pm
@@ -974,21 +974,9 @@ returns the error, otherwise returns false.
=cut
sub replace {
- my $new = shift;
- my $old = shift;
-
- if (!defined($old)) {
- warn "[debug]$me replace called with no arguments; autoloading old record\n"
- if $DEBUG;
- my $primary_key = $new->dbdef_table->primary_key;
- if ( $primary_key ) {
- $old = qsearchs($new->table, { $primary_key => $new->$primary_key() } )
- or croak "can't find ". $new->table. ".$primary_key ".
- $new->$primary_key();
- } else {
- croak $new->table. " has no primary key; pass old record as argument";
- }
- }
+ my ($new, $old) = (shift, shift);
+
+ $old = $new->replace_old unless defined($old);
warn "[debug]$me $new ->replace $old\n" if $DEBUG;
@@ -1158,6 +1146,22 @@ sub replace {
}
+sub replace_old {
+ my( $self ) = shift;
+ warn "[$me] replace called with no arguments; autoloading old record\n"
+ if $DEBUG;
+
+ my $primary_key = $self->dbdef_table->primary_key;
+ if ( $primary_key ) {
+ $self->by_key( $self->$primary_key() ) #this is what's returned
+ or croak "can't find ". $self->table. ".$primary_key ".
+ $self->$primary_key();
+ } else {
+ croak $self->table. " has no primary key; pass old record as argument";
+ }
+
+}
+
=item rep
Depriciated (use replace instead).
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 255b344..33baa0a 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -867,13 +867,20 @@ sub tables_hashref {
'svc_domain' => {
'columns' => [
- 'svcnum', 'int', '', '', '', '',
- 'domain', 'varchar', '', $char_d, '', '',
- 'catchall', 'int', 'NULL', '', '', '',
+ 'svcnum', 'int', '', '', '', '',
+ 'domain', 'varchar', '', $char_d, '', '',
+ 'suffix', 'varchar', 'NULL', $char_d, '', '',
+ 'catchall', 'int', 'NULL', '', '', '',
+ 'parent_svcnum', 'int', 'NULL', '', '', '',
+ 'registrarnum', 'int', 'NULL', '', '', '',
+ 'registrarkey', 'varchar', 'NULL', '', '', '',
+ 'setup_date', @date_type, '', '',
+ 'renewal_interval', 'int', 'NULL', '', '', '',
+ 'expiration_date', @date_type, '', '',
],
'primary_key' => 'svcnum',
- 'unique' => [ ['domain'] ],
- 'index' => [],
+ 'unique' => [ ],
+ 'index' => [ ['domain'] ],
},
'domain_record' => {
@@ -890,6 +897,16 @@ sub tables_hashref {
'index' => [ ['svcnum'] ],
},
+ 'registrar' => {
+ 'columns' => [
+ 'registrarnum', 'serial', '', '', '', '',
+ 'registrarname', 'varchar', '', $char_d, '', '',
+ ],
+ 'primary_key' => 'registrarnum',
+ 'unique' => [],
+ 'index' => [],
+ },
+
'svc_forward' => {
'columns' => [
'svcnum', 'int', '', '', '', '',
diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm
index 0597a38..3348d67 100644
--- a/FS/FS/UI/Web.pm
+++ b/FS/FS/UI/Web.pm
@@ -1,6 +1,7 @@
package FS::UI::Web;
-use vars qw($DEBUG);
+use strict;
+use vars qw($DEBUG $me);
use FS::Conf;
use FS::Record qw(dbdef);
@@ -9,6 +10,11 @@ use FS::Record qw(dbdef);
#@ISA = qw( FS::UI );
$DEBUG = 0;
+$me = '[FS::UID::Web]';
+
+###
+# date parsing
+###
use Date::Parse;
sub parse_beginning_ending {
@@ -32,6 +38,109 @@ sub parse_beginning_ending {
( $beginning, $ending );
}
+=item svc_url
+
+Returns a service URL, first checking to see if there is a service-specific
+page to link to, otherwise to a generic service handling page. Options are
+passed as a list of name-value pairs, and include:
+
+=over 4
+
+=item * m - Mason request object ($m)
+
+=item * action - The action for which to construct "edit", "view", or "search"
+
+=item ** part_svc - Service definition (see L<FS::part_svc>)
+
+=item ** svcdb - Service table
+
+=item *** query - Query string
+
+=item *** svc - FS::cust_svc or FS::svc_* object
+
+=item ahref - Optional flag, if set true returns <A HREF="$url"> instead of just the URL.
+
+=back
+
+* Required fields
+
+** part_svc OR svcdb is required
+
+*** query OR svc is required
+
+=cut
+
+ # ##
+ # #required
+ # ##
+ # 'm' => $m, #mason request object
+ # 'action' => 'edit', #or 'view'
+ #
+ # 'part_svc' => $part_svc, #usual
+ # #OR
+ # 'svcdb' => 'svc_table',
+ #
+ # 'query' => #optional query string
+ # #OR
+ # 'svc' => $svc_x, #or $cust_svc, it just needs a svcnum
+ #
+ # ##
+ # #optional
+ # ##
+ # 'ahref' => 1, # if set true, returns <A HREF="$url">
+
+use FS::CGI qw(popurl);
+sub svc_url {
+ my %opt = @_;
+
+ #? return '' unless ref($opt{part_svc});
+
+ my $svcdb = $opt{svcdb} || $opt{part_svc}->svcdb;
+ my $query = exists($opt{query}) ? $opt{query} : $opt{svc}->svcnum;
+ my $url;
+ warn "$me [svc_url] checking for /$opt{action}/$svcdb.cgi component"
+ if $DEBUG;
+ if ( $opt{m}->interp->comp_exists("/$opt{action}/$svcdb.cgi") ) {
+ $url = "$svcdb.cgi?";
+ } else {
+
+ my $generic = $opt{action} eq 'search' ? 'cust_svc' : 'svc_Common';
+
+ $url = "$generic.html?svcdb=$svcdb;";
+ $url .= 'svcnum=' if $query =~ /^\d+(;|$)/;
+ }
+
+ my $p = popurl(2); #?
+ my $return = "$p$opt{action}/$url$query";
+
+ $return = qq!<A HREF="$return">! if $opt{ahref};
+
+ $return;
+}
+
+sub svc_link {
+ my($m, $part_svc, $cust_svc) = @_ or return '';
+ svc_X_link( $part_svc->svc, @_ );
+}
+
+sub svc_label_link {
+ my($m, $part_svc, $cust_svc) = @_ or return '';
+ svc_X_link( ($cust_svc->label)[1], @_ );
+}
+
+sub svc_X_link {
+ my ($x, $m, $part_svc, $cust_svc) = @_ or return '';
+ my $ahref = svc_url(
+ 'ahref' => 1,
+ 'm' => $m,
+ 'action' => 'view',
+ 'part_svc' => $part_svc,
+ 'svc' => $cust_svc,
+ );
+
+ "$ahref$x</A>";
+}
+
sub parse_lt_gt {
my($cgi, $field) = @_;
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index ebe4c24..32da745 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -1526,11 +1526,17 @@ Returns all packages (see L<FS::cust_pkg>) for this customer.
sub all_pkgs {
my $self = shift;
+
+ return $self->num_pkgs unless wantarray;
+
+ my @cust_pkg = ();
if ( $self->{'_pkgnum'} ) {
- values %{ $self->{'_pkgnum'}->cache };
+ @cust_pkg = values %{ $self->{'_pkgnum'}->cache };
} else {
- qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
+ @cust_pkg = qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
}
+
+ sort sort_packages @cust_pkg;
}
=item ncancelled_pkgs
@@ -1541,19 +1547,43 @@ Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
sub ncancelled_pkgs {
my $self = shift;
+
+ return $self->num_ncancelled_pkgs unless wantarray;
+
+ my @cust_pkg = ();
if ( $self->{'_pkgnum'} ) {
- grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
+
+ @cust_pkg = grep { ! $_->getfield('cancel') }
+ values %{ $self->{'_pkgnum'}->cache };
+
} else {
- @{ [ # force list context
+
+ @cust_pkg =
qsearch( 'cust_pkg', {
- 'custnum' => $self->custnum,
- 'cancel' => '',
- }),
+ 'custnum' => $self->custnum,
+ 'cancel' => '',
+ });
+ push @cust_pkg,
qsearch( 'cust_pkg', {
- 'custnum' => $self->custnum,
- 'cancel' => 0,
- }),
- ] };
+ 'custnum' => $self->custnum,
+ 'cancel' => 0,
+ });
+ }
+
+ sort sort_packages @cust_pkg;
+
+}
+
+# This should be generalized to use config options to determine order.
+sub sort_packages {
+ if ( $a->get('cancel') and $b->get('cancel') ) {
+ $a->pkgnum <=> $b->pkgnum;
+ } elsif ( $a->get('cancel') or $b->get('cancel') ) {
+ return -1 if $b->get('cancel');
+ return 1 if $a->get('cancel');
+ return 0;
+ } else {
+ $a->pkgnum <=> $b->pkgnum;
}
}
@@ -1602,8 +1632,11 @@ customer.
=cut
sub num_cancelled_pkgs {
- my $self = shift;
- $self->num_pkgs("cancel IS NOT NULL AND cust_pkg.cancel != 0");
+ shift->num_pkgs("cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0");
+}
+
+sub num_ncancelled_pkgs {
+ shift->num_pkgs("( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )");
}
sub num_pkgs {
@@ -1737,7 +1770,7 @@ sub _banned_pay_hashref {
{
'payby' => $payby2ban{$self->payby},
'payinfo' => md5_base64($self->payinfo),
- #'reason' =>
+ #don't ever *search* on reason! #'reason' =>
};
}
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index 6807baf..689ffed 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -1,7 +1,7 @@
package FS::cust_pkg;
use strict;
-use vars qw(@ISA $disable_agentcheck @SVCDB_CANCEL_SEQ $DEBUG);
+use vars qw(@ISA $disable_agentcheck $DEBUG);
use Tie::IxHash;
use FS::UID qw( getotaker dbh );
use FS::Misc qw( send_email );
@@ -15,6 +15,7 @@ use FS::pkg_svc;
use FS::cust_bill_pkg;
use FS::h_cust_svc;
use FS::reg_code;
+use FS::part_svc;
use FS::cust_pkg_reason;
# need to 'use' these instead of 'require' in sub { cancel, suspend, unsuspend,
@@ -34,14 +35,6 @@ $DEBUG = 0;
$disable_agentcheck = 0;
-# The order in which to unprovision services.
-@SVCDB_CANCEL_SEQ = qw( svc_external
- svc_www
- svc_forward
- svc_acct
- svc_domain
- svc_broadband );
-
sub _cache {
my $self = shift;
my ( $hashref, $cache ) = @_;
@@ -273,6 +266,10 @@ Calls
sub replace {
my( $new, $old, %options ) = @_;
+ # We absolutely have to have an old vs. new record to make this work.
+ if (!defined($old)) {
+ $old = qsearchs( 'cust_pkg', { 'pkgnum' => $new->pkgnum } );
+ }
#return "Can't (yet?) change pkgpart!" if $old->pkgpart != $new->pkgpart;
return "Can't change otaker!" if $old->otaker ne $new->otaker;
@@ -452,19 +449,18 @@ sub cancel {
my %svc;
foreach my $cust_svc (
- qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
+ #schwartz
+ map { $_->[0] }
+ sort { $a->[1] <=> $b->[1] }
+ map { [ $_, $_->svc_x->table_info->{'cancel_weight'} ]; }
+ qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
) {
- push @{ $svc{$cust_svc->part_svc->svcdb} }, $cust_svc;
- }
- foreach my $svcdb (@SVCDB_CANCEL_SEQ) {
- foreach my $cust_svc (@{ $svc{$svcdb} }) {
- my $error = $cust_svc->cancel;
+ my $error = $cust_svc->cancel;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "Error cancelling cust_svc: $error";
- }
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error cancelling cust_svc: $error";
}
}
@@ -762,6 +758,17 @@ sub calc_cancel {
$self->part_pkg->calc_cancel($self, @_);
}
+=item cust_bill_pkg
+
+Returns any invoice line items for this package (see L<FS::cust_bill_pkg>).
+
+=cut
+
+sub cust_bill_pkg {
+ my $self = shift;
+ qsearch( 'cust_bill_pkg', { 'pkgnum' => $self->pkgnum } );
+}
+
=item cust_svc [ SVCPART ]
Returns the services for this package, as FS::cust_svc objects (see
@@ -843,7 +850,7 @@ sub num_cust_svc {
=item available_part_svc
-Returns a list FS::part_svc objects representing services included in this
+Returns a list of FS::part_svc objects representing services included in this
package but not yet provisioned. Each FS::part_svc object also has an extra
field, I<num_avail>, which specifies the number of available services.
@@ -861,6 +868,86 @@ sub available_part_svc {
$self->part_pkg->pkg_svc;
}
+=item
+
+Returns a list of FS::part_svc objects representing provisioned and available
+services included in this package. Each FS::part_svc object also has the
+following extra fields:
+
+=over 4
+
+=item num_cust_svc (count)
+
+=item num_avail (quantity - count)
+
+=item cust_pkg_svc (services) - array reference containing the provisioned services, as cust_svc objects
+
+svcnum
+label -> ($cust_svc->label)[1]
+
+=back
+
+=cut
+
+sub part_svc {
+ my $self = shift;
+
+ #XXX some sort of sort order besides numeric by svcpart...
+ my @part_svc = sort { $a->svcpart <=> $b->svcpart } map {
+ my $pkg_svc = $_;
+ my $part_svc = $pkg_svc->part_svc;
+ my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
+ $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #more evil
+ $part_svc->{'Hash'}{'num_avail'} = $pkg_svc->quantity - $num_cust_svc;
+ $part_svc->{'Hash'}{'cust_pkg_svc'} = [ $self->cust_svc($part_svc->svcpart) ];
+ $part_svc;
+ } $self->part_pkg->pkg_svc;
+
+ #extras
+ push @part_svc, map {
+ my $part_svc = $_;
+ my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
+ $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #speak no evail
+ $part_svc->{'Hash'}{'num_avail'} = 0; #0-$num_cust_svc ?
+ $part_svc->{'Hash'}{'cust_pkg_svc'} = [ $self->cust_svc($part_svc->svcpart) ];
+ $part_svc;
+ } $self->extra_part_svc;
+
+ @part_svc;
+
+}
+
+=item extra_part_svc
+
+Returns a list of FS::part_svc objects corresponding to services in this
+package which are still provisioned but not (any longer) available in the
+package definition.
+
+=cut
+
+sub extra_part_svc {
+ my $self = shift;
+
+ my $pkgnum = $self->pkgnum;
+ my $pkgpart = $self->pkgpart;
+
+ qsearch( {
+ 'table' => 'part_svc',
+ 'hashref' => {},
+ 'extra_sql' => "WHERE 0 = ( SELECT COUNT(*) FROM pkg_svc
+ WHERE pkg_svc.svcpart = part_svc.svcpart
+ AND pkg_svc.pkgpart = $pkgpart
+ AND quantity > 0
+ )
+ AND 0 < ( SELECT count(*)
+ FROM cust_svc
+ LEFT JOIN cust_pkg using ( pkgnum )
+ WHERE cust_svc.svcpart = part_svc.svcpart
+ AND pkgnum = $pkgnum
+ )",
+ } );
+}
+
=item status
Returns a short status string for this package, currently:
diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm
index a760761..cdb34cd 100644
--- a/FS/FS/cust_svc.pm
+++ b/FS/FS/cust_svc.pm
@@ -2,24 +2,21 @@ package FS::cust_svc;
use strict;
use vars qw( @ISA $DEBUG $me $ignore_quantity );
-use Carp qw( carp cluck );
+use Carp;
use FS::Conf;
use FS::Record qw( qsearch qsearchs dbh );
use FS::cust_pkg;
use FS::part_pkg;
use FS::part_svc;
use FS::pkg_svc;
-use FS::svc_acct;
-use FS::svc_domain;
-use FS::svc_forward;
-use FS::svc_broadband;
-use FS::svc_phone;
-use FS::svc_external;
use FS::domain_record;
use FS::part_export;
use FS::cdr;
-@ISA = qw( FS::Record );
+#most FS::svc_ classes are autoloaded in svc_x emthod
+use FS::svc_acct; #this one is used in the cache stuff
+
+@ISA = qw( FS::cust_main_Mixin FS::Record );
$DEBUG = 0;
$me = '[cust_svc]';
@@ -289,54 +286,20 @@ sub label {
my $self = shift;
carp "FS::cust_svc::label called on $self" if $DEBUG;
my $svc_x = $self->svc_x
- or die "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
+ or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
+
$self->_svc_label($svc_x);
}
sub _svc_label {
my( $self, $svc_x ) = ( shift, shift );
- my $svcdb = $self->part_svc->svcdb;
-
- my $tag;
- if ( $svcdb eq 'svc_acct' ) {
- $tag = $svc_x->email(@_);
- } elsif ( $svcdb eq 'svc_forward' ) {
- if ( $svc_x->srcsvc ) {
- my $svc_acct = $svc_x->srcsvc_acct(@_);
- $tag = $svc_acct->email(@_);
- } else {
- $tag = $svc_x->src;
- }
- $tag .= '->';
- if ( $svc_x->dstsvc ) {
- my $svc_acct = $svc_x->dstsvc_acct(@_);
- $tag .= $svc_acct->email(@_);
- } else {
- $tag .= $svc_x->dst;
- }
- } elsif ( $svcdb eq 'svc_domain' ) {
- $tag = $svc_x->getfield('domain');
- } elsif ( $svcdb eq 'svc_www' ) {
- my $domain_record = $svc_x->domain_record(@_);
- $tag = $domain_record->zone;
- } elsif ( $svcdb eq 'svc_broadband' ) {
- $tag = $svc_x->ip_addr;
- } elsif ( $svcdb eq 'svc_phone' ) {
- $tag = $svc_x->phonenum; #XXX format it better
- } elsif ( $svcdb eq 'svc_external' ) {
- my $conf = new FS::Conf;
- if ( $conf->config('svc_external-display_type') eq 'artera_turbo' ) {
- $tag = sprintf('%010d', $svc_x->id). '-'.
- substr('0000000000'.uc($svc_x->title), -10);
- } else {
- $tag = $svc_x->id. ': '. $svc_x->title;
- }
- } else {
- cluck "warning: asked for label of unsupported svcdb; using svcnum";
- $tag = $svc_x->getfield('svcnum');
- }
- $self->part_svc->svc, $tag, $svcdb, $self->svcnum;
+ (
+ $self->part_svc->svc,
+ $svc_x->label(@_),
+ $self->part_svc->svcdb,
+ $self->svcnum
+ );
}
@@ -353,7 +316,7 @@ sub svc_x {
if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
$self->{'_svc_acct'};
} else {
- #require "FS/$svcdb.pm";
+ require "FS/$svcdb.pm";
warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
if $DEBUG;
diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm
index fc5258f..5b4e54c 100644
--- a/FS/FS/part_svc.pm
+++ b/FS/FS/part_svc.pm
@@ -2,6 +2,7 @@ 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;
@@ -11,7 +12,7 @@ use FS::cust_svc;
@ISA = qw(FS::Record);
-$DEBUG = 1;
+$DEBUG = 0;
=head1 NAME
@@ -500,6 +501,161 @@ 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$/ ) {
+ 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,
+ ;
+
+ # yuck. maybe this won't be so bad when virtual fields become real fields
+ my %vfields;
+ foreach my $svcdb (grep dbdef->table($_), keys %svc_defs ) {
+ eval "use FS::$svcdb;";
+ my $self = "FS::$svcdb"->new;
+ $vfields{$svcdb} = {};
+ foreach my $field ($self->virtual_fields) { # svc_Common::virtual_fields with a null svcpart returns all of them
+ my $pvf = $self->pvf($field);
+ my @list = $pvf->list;
+ if (scalar @list) {
+ $svc_defs{$svcdb}->{$field} = { desc => $pvf->label,
+ type => 'select',
+ select_list => \@list };
+ } else {
+ $svc_defs{$svcdb}->{$field} = $pvf->label;
+ } #endif
+ $vfields{$svcdb}->{$field} = $pvf;
+ warn "\$vfields{$svcdb}->{$field} = $pvf"
+ if $DEBUG;
+ } #next $field
+ } #next $svcdb
+
+ $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", "disabled", or "radius_usergroup_selector"
+
+=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
@@ -554,10 +710,7 @@ sub process {
}
@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()
)
} );
@@ -651,8 +804,8 @@ sub process_bulk_cust_svc {
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
diff --git a/FS/FS/pkg_svc.pm b/FS/FS/pkg_svc.pm
index 065ddbe..9f3a4a1 100644
--- a/FS/FS/pkg_svc.pm
+++ b/FS/FS/pkg_svc.pm
@@ -82,7 +82,9 @@ returns the error, otherwise returns false.
=cut
sub replace {
- my ( $new, $old ) = ( shift, shift );
+ my( $new, $old ) = ( shift, shift );
+
+ $old = $new->replace_old unless defined($old);
return "Can't change pkgpart!" if $old->pkgpart != $new->pkgpart;
return "Can't change svcpart!" if $old->svcpart != $new->svcpart;
diff --git a/FS/FS/registrar.pm b/FS/FS/registrar.pm
new file mode 100644
index 0000000..cf5dc49
--- /dev/null
+++ b/FS/FS/registrar.pm
@@ -0,0 +1,119 @@
+package FS::registrar;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs );
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::registrar - Object methods for registrar records
+
+=head1 SYNOPSIS
+
+ use FS::registrar;
+
+ $record = new FS::registrar \%hash;
+ $record = new FS::registrar { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::registrar object represents a registrar. FS::registrar inherits from
+FS::Record. The following fields are currently supported:
+
+=over 4
+
+=item registrarnum - primary key
+
+=item registrarname -
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new registrar. To add the registrar to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'registrar'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid registrar. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('registrarnum')
+ || $self->ut_text('registrarname')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm
index 72e7d5c..f60a7d9 100644
--- a/FS/FS/svc_Common.pm
+++ b/FS/FS/svc_Common.pm
@@ -2,7 +2,7 @@ package FS::svc_Common;
use strict;
use vars qw( @ISA $noexport_hack $DEBUG $me );
-use Carp;
+use Carp qw( cluck carp croak ); #specify cluck have to specify them all..
use FS::Record qw( qsearch qsearchs fields dbh );
use FS::cust_main_Mixin;
use FS::cust_svc;
@@ -36,6 +36,27 @@ inherit from, i.e. FS::svc_acct. FS::svc_Common inherits from FS::Record.
=over 4
+=item search_sql_field FIELD STRING
+
+Class method which returns an SQL fragment to search for STRING in FIELD.
+
+=cut
+
+sub search_sql_field {
+ my( $class, $field, $string ) = @_;
+ my $table = $class->table;
+ my $q_string = dbh->quote($string);
+ "$table.$field = $q_string";
+}
+
+#fallback for services that don't provide a search...
+sub search_sql {
+ #my( $class, $string ) = @_;
+ '1 = 0'; #false
+}
+
+=item new
+
=cut
sub new {
@@ -114,6 +135,19 @@ sub virtual_fields {
return ();
}
+=item label
+
+svc_Common provides a fallback label subroutine that just returns the svcnum.
+
+=cut
+
+sub label {
+ my $self = shift;
+ cluck "warning: ". ref($self). " not loaded or missing label method; ".
+ "using svcnum";
+ $self->svcnum;
+}
+
=item check
Checks the validity of fields in this record.
@@ -300,35 +334,15 @@ sub delete {
local $SIG{TSTP} = 'IGNORE';
local $SIG{PIPE} = 'IGNORE';
- my $svcnum = $self->svcnum;
-
my $oldAutoCommit = $FS::UID::AutoCommit;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- $error = $self->SUPER::delete;
- return $error if $error;
-
- #new-style exports!
- unless ( $noexport_hack ) {
- foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
- $error = $part_export->export_delete($self);
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "exporting to ". $part_export->exporttype.
- " (transaction rolled back): $error";
- }
- }
- }
-
- $error = $self->return_inventory;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "error returning inventory: $error";
- }
-
- my $cust_svc = $self->cust_svc;
- $error = $cust_svc->delete;
+ $error = $self->SUPER::delete
+ || $self->export('delete')
+ || $self->return_inventory
+ || $self->cust_svc->delete
+ ;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
@@ -361,18 +375,7 @@ sub replace {
my $dbh = dbh;
# We absolutely have to have an old vs. new record to make this work.
- if ( !defined($old) ) {
- warn "[$me] replace called with no arguments; autoloading old record\n"
- if $DEBUG;
- my $primary_key = $new->dbdef_table->primary_key;
- if ( $primary_key ) {
- $old = qsearchs($new->table, { $primary_key => $new->$primary_key() } )
- or croak "can't find ". $new->table. ".$primary_key ".
- $new->$primary_key();
- } else {
- croak $new->table. " has no primary key; pass old record as argument";
- }
- }
+ $old = $new->replace_old unless defined($old);
my $error = $new->set_auto_inventory;
if ( $error ) {
@@ -678,33 +681,7 @@ Runs export_suspend callbacks.
sub suspend {
my $self = shift;
-
- local $SIG{HUP} = 'IGNORE';
- local $SIG{INT} = 'IGNORE';
- local $SIG{QUIT} = 'IGNORE';
- local $SIG{TERM} = 'IGNORE';
- local $SIG{TSTP} = 'IGNORE';
- local $SIG{PIPE} = 'IGNORE';
-
- my $oldAutoCommit = $FS::UID::AutoCommit;
- local $FS::UID::AutoCommit = 0;
- my $dbh = dbh;
-
- #new-style exports!
- unless ( $noexport_hack ) {
- foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
- my $error = $part_export->export_suspend($self);
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "error exporting to ". $part_export->exporttype.
- " (transaction rolled back): $error";
- }
- }
- }
-
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
- '';
-
+ $self->export('suspend');
}
=item unsuspend
@@ -715,6 +692,19 @@ Runs export_unsuspend callbacks.
sub unsuspend {
my $self = shift;
+ $self->export('unsuspend');
+}
+
+=item export HOOK [ EXPORT_ARGS ]
+
+Runs the provided export hook (i.e. "suspend", "unsuspend") for this service.
+
+=cut
+
+sub export {
+ my( $self, $method ) = ( shift, shift );
+
+ $method = "export_$method" unless $method =~ /^export_/;
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
@@ -730,10 +720,11 @@ sub unsuspend {
#new-style exports!
unless ( $noexport_hack ) {
foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
- my $error = $part_export->export_unsuspend($self);
+ next unless $part_export->can($method);
+ my $error = $part_export->$method($self, @_);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "error exporting to ". $part_export->exporttype.
+ return "error exporting $method event to ". $part_export->exporttype.
" (transaction rolled back): $error";
}
}
@@ -787,6 +778,8 @@ sub clone_kludge_unsuspend {
The setfixed method return value.
+B<export> method isn't used by insert and replace methods yet.
+
=head1 SEE ALSO
L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, schema.html
diff --git a/FS/FS/svc_External_Common.pm b/FS/FS/svc_External_Common.pm
new file mode 100644
index 0000000..a5805aa
--- /dev/null
+++ b/FS/FS/svc_External_Common.pm
@@ -0,0 +1,199 @@
+package FS::svc_External_Common;
+
+use strict;
+use vars qw(@ISA);
+use FS::svc_Common;
+
+@ISA = qw( FS::svc_Common );
+
+=head1 NAME
+
+FS::svc_external - Object methods for svc_external records
+
+=head1 SYNOPSIS
+
+ use FS::svc_external;
+
+ $record = new FS::svc_external \%hash;
+ $record = new FS::svc_external { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+ $error = $record->suspend;
+
+ $error = $record->unsuspend;
+
+ $error = $record->cancel;
+
+=head1 DESCRIPTION
+
+FS::svc_External_Common is intended as a base class for table-specific classes
+to inherit from. FS::svc_External_Common is used for services which connect
+to externally tracked services via "id" and "table" fields.
+
+FS::svc_External_Common inherits from FS::svc_Common.
+
+The following fields are currently supported:
+
+=over 4
+
+=item svcnum - primary key
+
+=item id - unique number of external record
+
+=item title - for invoice line items
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item search_sql
+
+Provides a default search_sql method which returns an SQL fragment to search
+the B<title> field.
+
+=cut
+
+sub search_sql {
+ my($class, $string) = @_;
+ $class->search_sql_field('title', $string);
+}
+
+=item new HASHREF
+
+Creates a new external service. To add the external service to the database,
+see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+=item label
+
+Returns a string identifying this external service in the form "id:title"
+
+=cut
+
+sub label {
+ my $self = shift;
+ $self->id. ':'. $self->title;
+}
+
+=item insert [ , OPTION => VALUE ... ]
+
+Adds this external service to the database. If there is an error, returns the
+error, otherwise returns false.
+
+The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
+defined. An FS::cust_svc record will be created and inserted.
+
+Currently available options are: I<depend_jobnum>
+
+If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
+jobnums), all provisioning jobs will have a dependancy on the supplied
+jobnum(s) (they will not run until the specific job(s) complete(s)).
+
+=cut
+
+#sub insert {
+# my $self = shift;
+# my $error;
+#
+# $error = $self->SUPER::insert(@_);
+# return $error if $error;
+#
+# '';
+#}
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+#sub delete {
+# my $self = shift;
+# my $error;
+#
+# $error = $self->SUPER::delete;
+# return $error if $error;
+#
+# '';
+#}
+
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+#sub replace {
+# my ( $new, $old ) = ( shift, shift );
+# my $error;
+#
+# $error = $new->SUPER::replace($old);
+# return $error if $error;
+#
+# '';
+#}
+
+=item suspend
+
+Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item unsuspend
+
+Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item cancel
+
+Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item check
+
+Checks all fields to make sure this is a valid external service. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $x = $self->setfixed;
+ return $x unless ref($x);
+ my $part_svc = $x;
+
+ my $error =
+ $self->ut_numbern('svcnum')
+ || $self->ut_numbern('id')
+ || $self->ut_textn('title')
+ ;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_Common>, L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>,
+L<FS::cust_pkg>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/svc_Parent_Mixin.pm b/FS/FS/svc_Parent_Mixin.pm
new file mode 100644
index 0000000..4501baf
--- /dev/null
+++ b/FS/FS/svc_Parent_Mixin.pm
@@ -0,0 +1,103 @@
+package FS::svc_Parent_Mixin;
+
+use strict;
+use NEXT;
+use FS::Record qw(qsearch qsearchs);
+use FS::cust_svc;
+
+=head1 NAME
+
+FS::svc_Parent_Mixin - Mixin class for svc_ classes with a parent_svcnum field
+
+=head1 SYNOPSIS
+
+package FS::svc_table;
+use vars qw(@ISA);
+@ISA = qw( FS::svc_Parent_Mixin FS::svc_Common );
+
+=head1 DESCRIPTION
+
+This is a mixin class for svc_ classes that contain a parent_svcnum field.
+
+=cut
+
+=head1 METHODS
+
+=over 4
+
+=item parent_cust_svc
+
+Returns the parent FS::cust_svc object.
+
+=cut
+
+sub parent_cust_svc {
+ my $self = shift;
+ qsearchs('cust_svc', { 'svcnum' => $self->parent_svcnum } );
+}
+
+=item parent_svc_x
+
+Returns the corresponding parent FS::svc_ object.
+
+=cut
+
+sub parent_svc_x {
+ my $self = shift;
+ $self->parent_cust_svc->svc_x;
+}
+
+=item children_cust_svc
+
+Returns a list of any child FS::cust_svc objects.
+
+Note: This is not recursive; it only returns direct children.
+
+=cut
+
+sub children_cust_svc {
+ my $self = shift;
+ qsearch('cust_svc', { 'parent_svcnum' => $self->svcnum } );
+}
+
+=item children_svc_x
+
+Returns the corresponding list of child FS::svc_ objects.
+
+=cut
+
+sub children_svc_x {
+ my $self = shift;
+ map { $_->svc_x } $self->children_cust_svc;
+}
+
+=item check
+
+This class provides a check subroutine which takes care of checking the
+parent_svcnum field. The svc_ class which uses it will call SUPER::check at
+the end of its own checks, and this class will call NEXT::check to pass
+the check "up the chain" (see L<NEXT>).
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ $self->ut_foreign_keyn('parent_svcnum', 'cust_svc', 'svcnum')
+ || $self->NEXT::check;
+
+}
+
+=back
+
+=head1 BUGS
+
+Do we need a recursive child finder for multi-layered children?
+
+=head1 SEE ALSO
+
+L<FS::svc_Common>, L<FS::Record>
+
+=cut
+
+1;
diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm
index 8c1c350..7cbe63f 100644
--- a/FS/FS/svc_acct.pm
+++ b/FS/FS/svc_acct.pm
@@ -210,6 +210,77 @@ Creates a new account. To add the account to the database, see L<"insert">.
=cut
+sub table_info {
+ {
+ 'name' => 'Account',
+ 'longname_plural' => 'Access accounts and mailboxes',
+ 'sorts' => [ 'username', 'uid', ],
+ 'display_weight' => 10,
+ 'cancel_weight' => 50,
+ 'fields' => {
+ 'dir' => 'Home directory',
+ 'uid' => {
+ label => 'UID',
+ def_label => 'UID (set to fixed and blank for no UIDs)',
+ type => 'text',
+ },
+ 'slipip' => 'IP address',
+ # 'popnum' => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
+ 'popnum' => {
+ label => 'Access number',
+ type => 'select',
+ select_table => 'svc_acct_pop',
+ select_key => 'popnum',
+ select_label => 'city',
+ },
+ 'username' => {
+ label => 'Username',
+ type => 'text',
+ disable_default => 1,
+ disable_fixed => 1,
+ },
+ 'quota' => {
+ label => 'Quota',
+ type => 'text',
+ disable_inventory => 1,
+ },
+ '_password' => 'Password',
+ 'gid' => {
+ label => 'GID',
+ def_label => 'GID (when blank, defaults to UID)',
+ type => 'text',
+ },
+ 'shell' => {
+ #desc =>'Shell (all service definitions should have a default or fixed shell that is present in the <b>shells</b> configuration file, set to blank for no shell tracking)',
+ label => 'Shell',
+ def_label=> 'Shell (set to blank for no shell tracking)',
+ type =>'select',
+ select_list => [ $conf->config('shells') ],
+ disable_inventory => 1,
+ },
+ 'finger' => 'Real name (GECOS)',
+ 'domsvc' => {
+ label => 'Domain',
+ def_label => 'svcnum from svc_domain',
+ type => 'select',
+ select_table => 'svc_domain',
+ select_key => 'svcnum',
+ select_label => 'domain',
+ disable_inventory => 1,
+ },
+ 'usergroup' => {
+ label => 'RADIUS groups',
+ type => 'radius_usergroup_selector',
+ disable_inventory => 1,
+ },
+ 'seconds' => { label => 'Seconds',
+ type => 'text',
+ disable_inventory => 1,
+ },
+ },
+ };
+}
+
sub table { 'svc_acct'; }
sub _fieldhandlers {
@@ -228,6 +299,52 @@ sub _fieldhandlers {
};
}
+=item search_sql STRING
+
+Class method which returns an SQL fragment to search for the given string.
+
+=cut
+
+sub search_sql {
+ my( $class, $string ) = @_;
+ if ( $string =~ /^([^@]+)@([^@]+)$/ ) {
+ my( $username, $domain ) = ( $1, $2 );
+ my $q_username = dbh->quote($username);
+ my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } );
+ if ( @svc_domain ) {
+ "svc_acct.username = $q_username AND ( ".
+ join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ).
+ " )";
+ } else {
+ '1 = 0'; #false
+ }
+ } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
+ ' ( '.
+ $class->search_sql_field('slipip', $string ).
+ ' OR '.
+ $class->search_sql_field('username', $string ).
+ ' ) ';
+ } else {
+ $class->search_sql_field('username', $string);
+ }
+}
+
+=item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
+
+Returns the "username@domain" string for this account.
+
+END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
+history records.
+
+=cut
+
+sub label {
+ my $self = shift;
+ $self->email(@_);
+}
+
+=cut
+
=item insert [ , OPTION => VALUE ... ]
Adds this account to the database. If there is an error, returns the error,
@@ -1180,10 +1297,13 @@ sub forget_snapshot {
}
-=item domain
+=item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
Returns the domain associated with this account.
+END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
+history records.
+
=cut
sub domain {
@@ -1201,6 +1321,8 @@ L<FS::svc_domain>).
=cut
+# FS::h_svc_acct has a history-aware svc_domain override
+
sub svc_domain {
my $self = shift;
$self->{'_domsvc'}
@@ -1216,10 +1338,13 @@ Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
#inherited from svc_Common
-=item email
+=item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
Returns an email address associated with the account.
+END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
+history records.
+
=cut
sub email {
diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm
index 5c9fe5e..b047c9a 100755
--- a/FS/FS/svc_broadband.pm
+++ b/FS/FS/svc_broadband.pm
@@ -85,8 +85,50 @@ points to. You can ask the object for a copy with the I<hash> method.
=cut
+sub table_info {
+ {
+ 'name' => 'Broadband',
+ 'name_plural' => 'Broadband services',
+ 'longname_plural' => 'Fixed (username-less) broadband services',
+ 'display_weight' => 50,
+ 'cancel_weight' => 70,
+ 'fields' => {
+ 'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.',
+ 'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',
+ 'ip_addr' => 'IP address. Leave blank for automatic assignment.',
+ 'blocknum' => 'Address block.',
+ },
+ };
+}
+
sub table { 'svc_broadband'; }
+=item search_sql STRING
+
+Class method which returns an SQL fragment to search for the given string.
+
+=cut
+
+sub search_sql {
+ my( $class, $string ) = @_;
+ if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
+ $class->search_sql_field('ip_addr', $string );
+ } else {
+ '1 = 0'; #false
+ }
+}
+
+=item label
+
+Returns the IP address.
+
+=cut
+
+sub label {
+ my $self = shift;
+ $self->ip_addr;
+}
+
=item insert [ , OPTION => VALUE ... ]
Adds this record to the database. If there is an error, returns the error,
diff --git a/FS/FS/svc_domain.pm b/FS/FS/svc_domain.pm
index bdaf79b..157f9e0 100644
--- a/FS/FS/svc_domain.pm
+++ b/FS/FS/svc_domain.pm
@@ -11,6 +11,7 @@ use Date::Format;
use FS::Record qw(fields qsearch qsearchs dbh);
use FS::Conf;
use FS::svc_Common;
+use FS::svc_Parent_Mixin;
use FS::cust_svc;
use FS::svc_acct;
use FS::cust_pkg;
@@ -18,7 +19,7 @@ use FS::cust_main;
use FS::domain_record;
use FS::queue;
-@ISA = qw( FS::svc_Common );
+@ISA = qw( FS::svc_Parent_Mixin FS::svc_Common );
#ask FS::UID to run this stuff for us later
$FS::UID::callback{'FS::domain'} = sub {
@@ -72,6 +73,20 @@ FS::svc_Common. The following fields are currently supported:
=item catchall - optional svcnum of an svc_acct record, designating an email catchall account.
+=item suffix -
+
+=item parent_svcnum -
+
+=item registrarnum - Registrar (see L<FS::registrar>)
+
+=item registrarkey - Registrar key or password for this domain
+
+=item setup_date - UNIX timestamp
+
+=item renewal_interval - Number of days before expiration date to start renewal
+
+=item expiration_date - UNIX timestamp
+
=back
=head1 METHODS
@@ -84,8 +99,37 @@ Creates a new domain. To add the domain to the database, see L<"insert">.
=cut
+sub table_info {
+ {
+ 'name' => 'Domain',
+ 'sorts' => 'domain',
+ 'display_weight' => 20,
+ 'cancel_weight' => 60,
+ 'fields' => {
+ 'domain' => 'Domain',
+ },
+ };
+}
+
sub table { 'svc_domain'; }
+sub search_sql {
+ my($class, $string) = @_;
+ $class->search_sql_field('domain', $string);
+}
+
+
+=item label
+
+Returns the domain.
+
+=cut
+
+sub label {
+ my $self = shift;
+ $self->domain;
+}
+
=item insert [ , OPTION => VALUE ... ]
Adds this domain to the database. If there is an error, returns the error,
@@ -141,15 +185,6 @@ sub insert {
return "Domain in use (here)"
if qsearchs( 'svc_domain', { 'domain' => $self->domain } );
- my $whois = $self->whois;
- if ( $self->action eq "N" && ! $whois_hack && $whois ) {
- $dbh->rollback if $oldAutoCommit;
- return "Domain in use (see whois)";
- }
- if ( $self->action eq "M" && ! $whois ) {
- $dbh->rollback if $oldAutoCommit;
- return "Domain not found (see whois)";
- }
$error = $self->SUPER::insert(@_);
if ( $error ) {
@@ -157,8 +192,6 @@ sub insert {
return $error;
}
- $self->submit_internic unless $whois_hack;
-
if ( $soamachine ) {
my $soa = new FS::domain_record {
'svcnum' => $self->svcnum,
@@ -257,6 +290,9 @@ returns the error, otherwise returns false.
sub replace {
my ( $new, $old ) = ( shift, shift );
+ # We absolutely have to have an old vs. new record to make this work.
+ $old = $new->replace_old unless defined($old);
+
return "Can't change domain - reorder."
if $old->getfield('domain') ne $new->getfield('domain');
@@ -317,45 +353,32 @@ sub check {
my($recref) = $self->hashref;
- unless ( $whois_hack ) {
- unless ( $self->email ) { #find out an email address
- my @svc_acct;
- foreach ( qsearch( 'cust_svc', { 'pkgnum' => $pkgnum } ) ) {
- my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $_->svcnum } );
- push @svc_acct, $svc_acct if $svc_acct;
- }
-
- if ( scalar(@svc_acct) == 0 ) {
- return "Must order an account in package ". $pkgnum. " first";
- } elsif ( scalar(@svc_acct) > 1 ) {
- return "More than one account in package ". $pkgnum. ": specify admin contact email";
- } else {
- $self->email($svc_acct[0]->email );
- }
- }
- }
-
#if ( $recref->{domain} =~ /^([\w\-\.]{1,22})\.(com|net|org|edu)$/ ) {
- if ( $recref->{domain} =~ /^([\w\-]{1,63})\.(com|net|org|edu)$/ ) {
+ if ( $recref->{domain} =~ /^([\w\-]{1,63})\.(com|net|org|edu|tv|info|biz)$/ ) {
$recref->{domain} = "$1.$2";
+ $recref->{suffix} ||= $2;
# hmmmmmmmm.
- } elsif ( $whois_hack && $recref->{domain} =~ /^([\w\-\.]+)$/ ) {
- $recref->{domain} = $1;
+ } elsif ( $whois_hack && $recref->{domain} =~ /^([\w\-\.]+)\.(\w+)$/ ) {
+ $recref->{domain} = "$1.$2";
+ # need to match a list of suffixes - no guarantee they're top-level..
} else {
return "Illegal domain ". $recref->{domain}.
" (or unknown registry - try \$whois_hack)";
}
- $recref->{action} =~ /^(M|N)$/
- or return "Illegal action: ". $recref->{action};
- $recref->{action} = $1;
if ( $recref->{catchall} ne '' ) {
my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $recref->{catchall} } );
return "Unknown catchall" unless $svc_acct;
}
- $self->ut_textn('purpose')
+ $self->ut_alphan('suffix')
+ or $self->ut_foreign_keyn('registrarnum', 'registrar', 'registrarnum')
+ or $self->ut_textn('registrarkey')
+ or $self->ut_numbern('setup_date')
+ or $self->ut_numbern('renewal_interval')
+ or $self->ut_numbern('expiration_date')
+ or $self->ut_textn('purpose')
or $self->SUPER::check;
}
@@ -402,7 +425,7 @@ sub catchall_svc_acct {
sub whois {
#$whois_hack or new Net::Whois::Domain $_[0]->domain;
- $whois_hack or die "whois_hack not set...\n";
+ #$whois_hack or die "whois_hack not set...\n";
}
=item _whois
diff --git a/FS/FS/svc_external.pm b/FS/FS/svc_external.pm
index 14eab7e..5aaee48 100644
--- a/FS/FS/svc_external.pm
+++ b/FS/FS/svc_external.pm
@@ -1,16 +1,11 @@
package FS::svc_external;
use strict;
-use vars qw(@ISA); # $conf
-use FS::UID;
-#use FS::Record qw( qsearch qsearchs dbh);
-use FS::svc_Common;
+use vars qw(@ISA);
+use FS::Conf;
+use FS::svc_External_Common;
-@ISA = qw( FS::svc_Common );
-
-#FS::UID::install_callback( sub {
-# $conf = new FS::Conf;
-#};
+@ISA = qw( FS::svc_External_Common );
=head1 NAME
@@ -39,9 +34,9 @@ FS::svc_external - Object methods for svc_external records
=head1 DESCRIPTION
-An FS::svc_external object represents a externally tracked service.
-FS::svc_external inherits from FS::svc_Common. The following fields are
-currently supported:
+An FS::svc_external object represents a generic externally tracked service.
+FS::svc_external inherits from FS::svc_External_Common (and FS::svc_Common).
+The following fields are currently supported:
=over 4
@@ -67,8 +62,31 @@ points to. You can ask the object for a copy with the I<hash> method.
=cut
+sub table_info {
+ {
+ 'name' => 'External service',
+ 'sorts' => 'id',
+ 'display_weight' => 90,
+ 'cancel_weight' => 10,
+ 'fields' => {
+ },
+ };
+}
+
sub table { 'svc_external'; }
+# oh! this should be moved to svc_artera_turbo or something now
+sub label {
+ my $self = shift;
+ my $conf = new FS::Conf;
+ if ( $conf->config('svc_external-display_type') eq 'artera_turbo' ) {
+ sprintf('%010d', $self->id). '-'.
+ substr('0000000000'.uc($self->title), -10);
+ } else {
+ $self->SUPER::label;
+ }
+}
+
=item insert [ , OPTION => VALUE ... ]
Adds this external service to the database. If there is an error, returns the
@@ -149,21 +167,15 @@ and replace methods.
=cut
-sub check {
- my $self = shift;
-
- my $x = $self->setfixed;
- return $x unless ref($x);
- my $part_svc = $x;
-
- my $error =
- $self->ut_numbern('svcnum')
- || $self->ut_numbern('id')
- || $self->ut_textn('title')
- ;
-
- $self->SUPER::check;
-}
+#sub check {
+# my $self = shift;
+# my $error;
+#
+# $error = $self->SUPER::delete;
+# return $error if $error;
+#
+# '';
+#}
=back
@@ -171,8 +183,8 @@ sub check {
=head1 SEE ALSO
-L<FS::svc_Common>, L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>,
-L<FS::cust_pkg>, schema.html from the base documentation.
+L<FS::svc_External_Common>, L<FS::svc_Common>, L<FS::Record>, L<FS::cust_svc>,
+L<FS::part_svc>, L<FS::cust_pkg>, schema.html from the base documentation.
=cut
diff --git a/FS/FS/svc_forward.pm b/FS/FS/svc_forward.pm
index ab24d32..91e251f 100644
--- a/FS/FS/svc_forward.pm
+++ b/FS/FS/svc_forward.pm
@@ -66,8 +66,67 @@ database, see L<"insert">.
=cut
+
+sub table_info {
+ {
+ 'name' => 'Forward',
+ 'name_plural' => 'Mail forwards',
+ 'display_weight' => 30,
+ 'cancel_weight' => 30,
+ 'fields' => {
+ 'srcsvc' => 'service from which mail is to be forwarded',
+ 'dstsvc' => 'service to which mail is to be forwarded',
+ 'dst' => 'someone@another.domain.com to use when dstsvc is 0',
+ },
+ };
+}
+
sub table { 'svc_forward'; }
+=item search_sql STRING
+
+Class method which returns an SQL fragment to search for the given string.
+
+=cut
+
+sub search_sql {
+ my( $class, $string ) = @_;
+ $class->search_sql_field('src', $string);
+}
+
+=item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
+
+Returns a text string representing this forward.
+
+END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
+history records.
+
+=cut
+
+sub label {
+ my $self = shift;
+ my $tag = '';
+
+ if ( $self->srcsvc ) {
+ my $svc_acct = $self->srcsvc_acct(@_);
+ $tag = $svc_acct->email(@_);
+ } else {
+ $tag = $self->src;
+ }
+
+ $tag .= ' -> ';
+
+ if ( $self->dstsvc ) {
+ my $svc_acct = $self->dstsvc_acct(@_);
+ $tag .= $svc_acct->email(@_);
+ } else {
+ $tag .= $self->dst;
+ }
+
+ $tag;
+}
+
+
=item insert [ , OPTION => VALUE ... ]
Adds this mail forwarding alias to the database. If there is an error, returns
diff --git a/FS/FS/svc_phone.pm b/FS/FS/svc_phone.pm
index fca3369..8e39b9f 100644
--- a/FS/FS/svc_phone.pm
+++ b/FS/FS/svc_phone.pm
@@ -63,9 +63,51 @@ points to. You can ask the object for a copy with the I<hash> method.
=cut
# the new method can be inherited from FS::Record, if a table method is defined
+#
+sub table_info {
+ {
+ 'name' => 'Phone number',
+ 'sorts' => 'phonenum',
+ 'display_weight' => 60,
+ 'cancel_weight' => 80,
+ 'fields' => {
+ 'countrycode' => { label => 'Country code',
+ type => 'text',
+ disable_inventory => 1,
+ },
+ 'phonenum' => 'Phone number',
+ 'pin' => { label => 'Personal Identification Number',
+ type => 'text',
+ disable_inventory => 1,
+ },
+ },
+ };
+}
sub table { 'svc_phone'; }
+=item search_sql STRING
+
+Class method which returns an SQL fragment to search for the given string.
+
+=cut
+
+sub search_sql {
+ my( $class, $string ) = @_;
+ $class->search_sql_field('phonenum', $string );
+}
+
+=item label
+
+Returns the phone number.
+
+=cut
+
+sub label {
+ my $self = shift;
+ $self->phonenum; #XXX format it better
+}
+
=item insert
Adds this record to the database. If there is an error, returns the error,
diff --git a/FS/FS/svc_www.pm b/FS/FS/svc_www.pm
index 7c7032f..066719b 100644
--- a/FS/FS/svc_www.pm
+++ b/FS/FS/svc_www.pm
@@ -72,8 +72,33 @@ points to. You can ask the object for a copy with the I<hash> method.
=cut
+sub table_info {
+ {
+ 'name' => 'Hosting',
+ 'name_plural' => 'Virtual hosting services',
+ 'display_weight' => 40,
+ 'cancel_weight' => 20,
+ 'fields' => {
+ },
+ };
+};
+
sub table { 'svc_www'; }
+=item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
+
+Returns the zone name for this virtual host.
+
+END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
+history records.
+
+=cut
+
+sub label {
+ my $self = shift;
+ $self->domain_record(@_)->zone;
+}
+
=item insert [ , OPTION => VALUE ... ]
Adds this record to the database. If there is an error, returns the error,
diff --git a/FS/MANIFEST b/FS/MANIFEST
index 906cc9c..c5c40c6 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -355,6 +355,12 @@ FS/cust_bill_pay_pkg.pm
t/cust_bill_pay_pkg.t
FS/cust_credit_bill_pkg.pm
t/cust_credit_bill_pkg.t
+FS/registrar.pm
+t/registrar.t
+FS/svc_External_Common.pm
+t/svc_External_Common.t
+FS/svc_Parent_Mixin.pm
+t/svc_Parent_Mixin.t
FS/cust_main_note.pm
t/cust_main_note.t
FS/cust_pkg_reason.pm
diff --git a/FS/t/registrar.t b/FS/t/registrar.t
new file mode 100644
index 0000000..a6ba134
--- /dev/null
+++ b/FS/t/registrar.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::registrar;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_External_Common.t b/FS/t/svc_External_Common.t
new file mode 100644
index 0000000..a0b2ea2
--- /dev/null
+++ b/FS/t/svc_External_Common.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_External_Common;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_Parent_Mixin.t b/FS/t/svc_Parent_Mixin.t
new file mode 100644
index 0000000..ed9923f
--- /dev/null
+++ b/FS/t/svc_Parent_Mixin.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_Parent_Mixin;
+$loaded=1;
+print "ok 1\n";