summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
authormark <mark>2011-04-01 02:52:24 +0000
committermark <mark>2011-04-01 02:52:24 +0000
commitb65b8096089410001dfbcd35f9a56f9405b9f5f1 (patch)
treedf16b0422007bd876bc969bdff857d8a510b0aca /FS
parent19b0c403fbf697a1de92f12abc8a2104daca07bc (diff)
svc_hardware and svc_dish, #11454
Diffstat (limited to 'FS')
-rw-r--r--FS/FS/Mason.pm5
-rw-r--r--FS/FS/Record.pm30
-rw-r--r--FS/FS/Schema.pm60
-rw-r--r--FS/FS/cust_pkg.pm2
-rw-r--r--FS/FS/cust_svc.pm14
-rw-r--r--FS/FS/hardware_class.pm127
-rw-r--r--FS/FS/hardware_status.pm116
-rw-r--r--FS/FS/hardware_type.pm131
-rw-r--r--FS/FS/part_svc.pm5
-rw-r--r--FS/FS/part_svc_column.pm10
-rw-r--r--FS/FS/svc_dish.pm131
-rw-r--r--FS/FS/svc_hardware.pm211
-rw-r--r--FS/MANIFEST10
-rw-r--r--FS/t/hardware_class.t5
-rw-r--r--FS/t/hardware_status.t5
-rw-r--r--FS/t/hardware_type.t5
-rw-r--r--FS/t/svc_dish.t5
-rw-r--r--FS/t/svc_hardware.t5
18 files changed, 872 insertions, 5 deletions
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index ccab79339..dd1871739 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -273,6 +273,11 @@ if ( -e $addl_handler_use_file ) {
use FS::torrus_srvderive;
use FS::torrus_srvderive_component;
use FS::areacode;
+ use FS::svc_dish;
+ use FS::svc_hardware;
+ use FS::hardware_class;
+ use FS::hardware_type;
+ use FS::hardware_status;
# Sammath Naur
if ( $FS::Mason::addl_handler_use ) {
diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm
index fb83faa3f..7b52f508e 100644
--- a/FS/FS/Record.pm
+++ b/FS/FS/Record.pm
@@ -21,6 +21,7 @@ use FS::CurrentUser;
use FS::Schema qw(dbdef);
use FS::SearchCache;
use FS::Msgcat qw(gettext);
+use NetAddr::IP; # for validation
#use FS::Conf; #dependency loop bs, in install_callback below instead
use FS::part_virtual_field;
@@ -2376,6 +2377,35 @@ sub ut_ipn {
}
}
+=item ut_ip46 COLUMN
+
+Check/untaint IPv4 or IPv6 address.
+
+=cut
+
+sub ut_ip46 {
+ my( $self, $field ) = @_;
+ my $ip = NetAddr::IP->new($self->getfield($field))
+ or return "Illegal (IP address) $field: ".$self->getfield($field);
+ $self->setfield($field, lc($ip->addr));
+ return '';
+}
+
+=item ut_ip46n
+
+Check/untaint IPv6 or IPv6 address. May be null.
+
+=cut
+
+sub ut_ip46n {
+ my( $self, $field ) = @_;
+ if ( $self->getfield($field) =~ /^$/ ) {
+ $self->setfield($field, '');
+ return '';
+ }
+ $self->ut_ip46($field);
+}
+
=item ut_coord COLUMN [ LOWER [ UPPER ] ]
Check/untaint coordinates.
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 25eafa345..66847b6c0 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -1586,7 +1586,7 @@ sub tables_hashref {
'index' => [ [ 'svcnum' ], [ 'optionname' ] ],
},
- 'part_pkg' => {
+ 'part_pkg' => {
'columns' => [
'pkgpart', 'serial', '', '', '', '',
'pkg', 'varchar', '', $char_d, '', '',
@@ -1745,6 +1745,7 @@ sub tables_hashref {
'svc', 'varchar', '', $char_d, '', '',
'svcdb', 'varchar', '', $char_d, '', '',
'disabled', 'char', 'NULL', 1, '', '',
+ 'preserve', 'char', 'NULL', 1, '', '',
],
'primary_key' => 'svcpart',
'unique' => [],
@@ -2003,6 +2004,63 @@ sub tables_hashref {
'index' => [ ['svcnum'] ],
},
+ 'svc_dish' => {
+ 'columns' => [
+ 'svcnum', 'int', '', '', '', '',
+ 'acctnum', 'varchar', '', 16, '', '',
+ 'note', 'text', 'NULL', '', '', '',
+ ],
+ 'primary_key' => 'svcnum',
+ 'unique' => [ ],
+ 'index' => [ ],
+ },
+
+ 'svc_hardware' => {
+ 'columns' => [
+ 'svcnum', 'int', '', '', '', '',
+ 'typenum', 'int', '', '', '', '',
+ 'serial', 'varchar', 'NULL', $char_d, '', '',
+ 'ip_addr', 'varchar', 'NULL', 40, '', '',
+ 'hw_addr', 'varchar', 'NULL', 12, '', '',
+ 'statusnum','int', 'NULL', '', '', '',
+ 'note', 'text', 'NULL', '', '', '',
+ ],
+ 'primary_key' => 'svcnum',
+ 'unique' => [ ],
+ 'index' => [ ],
+ },
+
+ 'hardware_class' => {
+ 'columns' => [
+ 'classnum', 'serial', '', '', '', '',
+ 'classname', 'varchar', '', $char_d, '', '',
+ ],
+ 'primary_key' => 'classnum',
+ 'unique' => [ ],
+ 'index' => [ ],
+ },
+
+ 'hardware_type' => {
+ 'columns' => [
+ 'typenum', 'serial', '', '', '', '',
+ 'classnum', 'int', '', '', '', '',
+ 'model', 'varchar', '', $char_d, '', '',
+ ],
+ 'primary_key' => 'typenum',
+ 'unique' => [ ],
+ 'index' => [ ],
+ },
+
+ 'hardware_status' => {
+ 'columns' => [
+ 'statusnum', 'serial', '', '', '', '',
+ 'label' ,'varchar', '', $char_d, '', '',
+ ],
+ 'primary_key' => 'statusnum',
+ 'unique' => [ ],
+ 'index' => [ ],
+ },
+
'domain_record' => {
'columns' => [
'recnum', 'serial', '', '', '', '',
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index 2efd4fd87..13a3b6eb7 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -761,6 +761,8 @@ sub cancel {
map { [ $_, $_->svc_x->table_info->{'cancel_weight'} ]; }
qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
) {
+ my $part_svc = $cust_svc->part_svc;
+ next if ( defined($part_svc) and $part_svc->preserve );
my $error = $cust_svc->cancel( %svc_cancel_opt );
if ( $error ) {
diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm
index 30e49c598..8cce7afb4 100644
--- a/FS/FS/cust_svc.pm
+++ b/FS/FS/cust_svc.pm
@@ -371,6 +371,20 @@ sub date_inserted {
$self->h_date('insert');
}
+=item pkg_cancel_date
+
+Returns the date this service's package was canceled. This normally only
+exists for a service that's been preserved through cancellation with the
+part_pkg.preserve flag.
+
+=cut
+
+sub pkg_cancel_date {
+ my $self = shift;
+ my $cust_pkg = $self->cust_pkg or return;
+ return $cust_pkg->getfield('cancel') || '';
+}
+
=item label
Returns a list consisting of:
diff --git a/FS/FS/hardware_class.pm b/FS/FS/hardware_class.pm
new file mode 100644
index 000000000..073a97f88
--- /dev/null
+++ b/FS/FS/hardware_class.pm
@@ -0,0 +1,127 @@
+package FS::hardware_class;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::hardware_class - Object methods for hardware_class records
+
+=head1 SYNOPSIS
+
+ use FS::hardware_class;
+
+ $record = new FS::hardware_class \%hash;
+ $record = new FS::hardware_class { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::hardware_class object represents a class of hardware types which can
+be assigned to similar services (see L<FS::svc_hardware>). FS::hardware_class
+inherits from FS::Record. The following fields are currently supported:
+
+=over 4
+
+=item classnum - primary key
+
+=item classname - classname
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record 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 { 'hardware_class'; }
+
+=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 hardware class. 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('classnum')
+ || $self->ut_text('classname')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=item hardware_type
+
+Returns all L<FS::hardware_type> objects belonging to this class.
+
+=cut
+
+sub hardware_type {
+ my $self = shift;
+ return qsearch('hardware_type', { 'classnum' => $self->classnum });
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::hardware_type>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/hardware_status.pm b/FS/FS/hardware_status.pm
new file mode 100644
index 000000000..4836fc5cb
--- /dev/null
+++ b/FS/FS/hardware_status.pm
@@ -0,0 +1,116 @@
+package FS::hardware_status;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::hardware_status - Object methods for hardware_status records
+
+=head1 SYNOPSIS
+
+ use FS::hardware_status;
+
+ $record = new FS::hardware_status \%hash;
+ $record = new FS::hardware_status { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::hardware_status object represents an installation status for hardware
+services. FS::hardware_status inherits from FS::Record. The following fields
+are currently supported:
+
+=over 4
+
+=item statusnum - primary key
+
+=item label - descriptive label
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record 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 { 'hardware_status'; }
+
+=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 status. 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('statusnum')
+ || $self->ut_text('label')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::svc_hardware>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/hardware_type.pm b/FS/FS/hardware_type.pm
new file mode 100644
index 000000000..ba19fcb21
--- /dev/null
+++ b/FS/FS/hardware_type.pm
@@ -0,0 +1,131 @@
+package FS::hardware_type;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::hardware_type - Object methods for hardware_type records
+
+=head1 SYNOPSIS
+
+ use FS::hardware_type;
+
+ $record = new FS::hardware_type \%hash;
+ $record = new FS::hardware_type { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::hardware_type object represents a device type (a model name or
+number) assignable as a hardware service (L<FS::svc_hardware)>).
+FS::hardware_type inherits from FS::Record. The following fields are
+currently supported:
+
+=over 4
+
+=item typenum - primary key
+
+=item classnum - key to an L<FS::hardware_class> record defining the class
+to which this device type belongs.
+
+=item model - descriptive model name or number
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record 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 { 'hardware_type'; }
+
+=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 hardware type. 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('typenum')
+ || $self->ut_foreign_key('classnum', 'hardware_class', 'classnum')
+ || $self->ut_text('model')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=item hardware_class
+
+Returns the L<FS::hardware_class> associated with this device.
+
+=cut
+
+sub hardware_class {
+ my $self = shift;
+ return qsearchs('hardware_class', { 'classnum' => $self->classnum });
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::svc_hardware>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm
index 164bad079..e15b22590 100644
--- a/FS/FS/part_svc.pm
+++ b/FS/FS/part_svc.pm
@@ -53,6 +53,8 @@ L<FS::svc_domain>, and L<FS::svc_forward>, among others.
=item disabled - Disabled flag, empty or `Y'
+=item preserve - Preserve after cancellation, empty or 'Y'
+
=back
=head1 METHODS
@@ -381,6 +383,7 @@ sub check {
|| $self->ut_text('svc')
|| $self->ut_alpha('svcdb')
|| $self->ut_enum('disabled', [ '', 'Y' ] )
+ || $self->ut_enum('preserve', [ '', 'Y' ] )
;
return $error if $error;
@@ -758,7 +761,7 @@ sub process {
map {
my $f = $svcdb.'__'.$_;
- if ( $param->{ $f.'_flag' } =~ /^[MA]$/ ) {
+ if ( $param->{ $f.'_flag' } =~ /^[MAH]$/ ) {
$param->{ $f } = delete( $param->{ $f.'_classnum' } );
}
if ( $param->{ $f.'_flag' } =~ /^S$/ ) {
diff --git a/FS/FS/part_svc_column.pm b/FS/FS/part_svc_column.pm
index f5b39c049..d467516ed 100644
--- a/FS/FS/part_svc_column.pm
+++ b/FS/FS/part_svc_column.pm
@@ -43,7 +43,7 @@ fields are currently supported:
=item columnvalue - default or fixed value for the column
-=item columnflag - 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 columnflag - null or empty (no default), `D' for default, `F' for fixed (unchangeable), `S' for selectable choice, `M' for manual selection from inventory, `A' for automatic selection from inventory, or `H' for selection from a hardware class. For virtual fields, can also be 'X' for excluded.
=back
@@ -94,15 +94,19 @@ sub check {
;
return $error if $error;
- $self->columnflag =~ /^([DFSMAX]?)$/
+ $self->columnflag =~ /^([DFSMAHX]?)$/
or return "illegal columnflag ". $self->columnflag;
$self->columnflag(uc($1));
if ( $self->columnflag =~ /^[MA]$/ ) {
$error =
$self->ut_foreign_key( 'columnvalue', 'inventory_class', 'classnum' );
- return $error if $error;
}
+ if ( $self->columnflag eq 'H' ) {
+ $error =
+ $self->ut_foreign_key( 'columnvalue', 'hardware_class', 'classnum' );
+ }
+ return $error if $error;
$self->SUPER::check;
}
diff --git a/FS/FS/svc_dish.pm b/FS/FS/svc_dish.pm
new file mode 100644
index 000000000..5dac4f4d5
--- /dev/null
+++ b/FS/FS/svc_dish.pm
@@ -0,0 +1,131 @@
+package FS::svc_dish;
+
+use strict;
+use base qw( FS::svc_Common );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::svc_dish - Object methods for svc_dish records
+
+=head1 SYNOPSIS
+
+ use FS::svc_dish;
+
+ $record = new FS::svc_dish \%hash;
+ $record = new FS::svc_dish { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::svc_dish object represents a Dish Network service. FS::svc_dish
+inherits from FS::svc_Common.
+
+The following fields are currently supported:
+
+=over 4
+
+=item svcnum - Primary key
+
+=item acctnum - DISH account number
+
+=item note - Installation notes: location on property, physical access, etc.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new svc_dish object.
+
+=cut
+
+sub table { 'svc_dish'; }
+
+sub table_info {
+ my %opts = ( 'type' => 'text',
+ 'disable_select' => 1,
+ 'disable_inventory' => 1,
+ );
+ {
+ 'name' => 'Dish service',
+ 'display_weight' => 58,
+ 'cancel_weight' => 85,
+ 'fields' => {
+ 'svcnum' => { label => 'Service' },
+ 'acctnum' => { label => 'DISH account#', %opts },
+ 'note' => { label => 'Installation notes', %opts },
+ }
+ }
+}
+
+sub label {
+ my $self = shift;
+ $self->acctnum;
+}
+
+sub search_sql {
+ my($class, $string) = @_;
+ $class->search_sql_field('acctnum', $string);
+}
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=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.
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid 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 $error =
+ $self->ut_numbern('svcnum')
+ || $self->ut_text('acctnum')
+ || $self->ut_textn('note')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::svc_Common>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/svc_hardware.pm b/FS/FS/svc_hardware.pm
new file mode 100644
index 000000000..5b81a9c2b
--- /dev/null
+++ b/FS/FS/svc_hardware.pm
@@ -0,0 +1,211 @@
+package FS::svc_hardware;
+
+use strict;
+use base qw( FS::svc_Common );
+use FS::Record qw( qsearch qsearchs );
+use FS::hardware_type;
+use FS::hardware_status;
+
+=head1 NAME
+
+FS::svc_hardware - Object methods for svc_hardware records
+
+=head1 SYNOPSIS
+
+ use FS::svc_hardware;
+
+ $record = new FS::svc_hardware \%hash;
+ $record = new FS::svc_hardware { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::svc_hardware object represents an equipment installation, such as
+a wireless broadband receiver, satellite antenna, or DVR. FS::svc_hardware
+inherits from FS::svc_Common.
+
+The following fields are currently supported:
+
+=over 4
+
+=item svcnum - Primary key
+
+=item typenum - Device type number (see L<FS::hardware_type>)
+
+=item ip_addr - IP address
+
+=item hw_addr - Hardware address
+
+=item serial - Serial number
+
+=item statusnum - Service status (see L<FS::hardware_status>)
+
+=item note - Installation notes: location on property, physical access, etc.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new svc_hardware object.
+
+=cut
+
+sub table { 'svc_hardware'; }
+
+sub table_info {
+ my %opts = ( 'type' => 'text', 'disable_select' => 1 );
+ {
+ 'name' => 'Hardware', #?
+ 'name_plural' => 'Hardware',
+ 'display_weight' => 59,
+ 'cancel_weight' => 86,
+ 'fields' => {
+ 'svcnum' => { label => 'Service' },
+ 'typenum' => { label => 'Device type',
+ type => 'select-hardware',
+ disable_select => 1,
+ disable_fixed => 1,
+ disable_default => 1,
+ disable_inventory => 1,
+ },
+ 'serial' => { label => 'Serial number', %opts },
+ 'hw_addr' => { label => 'Hardware address', %opts },
+ 'ip_addr' => { label => 'IP address', %opts },
+ 'statusnum' => { label => 'Service status',
+ type => 'select',
+ select_table => 'hardware_status',
+ select_key => 'statusnum',
+ select_label => 'label',
+ disable_inventory => 1,
+ },
+ 'note' => { label => 'Installation notes', %opts },
+ }
+ }
+}
+
+sub search_sql {
+ my ($class, $string) = @_;
+ my @where = ();
+
+ my $ip = NetAddr::IP->new($string);
+ if ( $ip ) {
+ push @where, $class->search_sql_field('ip_addr', $ip->addr);
+ }
+
+ if ( $string =~ /^(\w+)$/ ) {
+ push @where, 'LOWER(svc_hardware.serial) LIKE \'%'.lc($string).'%\'';
+ }
+
+ if ( $string =~ /^([0-9A-Fa-f]|\W)+$/ ) {
+ my $hex = uc($string);
+ $hex =~ s/\W//g;
+ push @where, 'svc_hardware.hw_addr LIKE \'%'.$hex.'%\'';
+ }
+ '(' . join(' OR ', @where) . ')';
+}
+
+sub label {
+ my $self = shift;
+ $self->serial || $self->hw_addr;
+}
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=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.
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid 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 $hw_addr = $self->getfield('hw_addr');
+ $hw_addr = join('', split(/\W/, $hw_addr));
+ $self->setfield('hw_addr', $hw_addr);
+
+ my $error =
+ $self->ut_numbern('svcnum')
+ || $self->ut_foreign_key('typenum', 'hardware_type', 'typenum')
+ || $self->ut_ip46n('ip_addr')
+ || $self->ut_hexn('hw_addr')
+ || $self->ut_alphan('serial')
+ || $self->ut_foreign_keyn('statusnum', 'hardware_status', 'statusnum')
+ || $self->ut_textn('note')
+ ;
+ return $error if $error;
+
+ if ( !length($self->getfield('hw_addr'))
+ and !length($self->getfield('serial')) ) {
+ return 'Serial number or hardware address required';
+ }
+
+ $self->SUPER::check;
+}
+
+=item hardware_type
+
+Returns the L<FS::hardware_type> object associated with this installation.
+
+=cut
+
+sub hardware_type {
+ my $self = shift;
+ return qsearchs('hardware_type', { 'typenum' => $self->typenum });
+}
+
+=item status_label
+
+Returns the 'label' field of the L<FS::hardware_status> object associated
+with this installation.
+
+=cut
+
+sub status_label {
+ my $self = shift;
+ my $status = qsearchs('hardware_status', { 'statusnum' => $self->statusnum })
+ or return '';
+ $status->label;
+}
+
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::svc_Common>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/MANIFEST b/FS/MANIFEST
index fce790346..a293451a1 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -578,3 +578,13 @@ FS/areacode.pm
t/areacode.t
FS/areacode.pm
t/areacode.t
+FS/svc_dish.pm
+t/svc_dish.t
+FS/svc_hardware.pm
+t/svc_hardware.t
+FS/hardware_class.pm
+t/hardware_class.t
+FS/hardware_type.pm
+t/hardware_type.t
+FS/hardware_status.pm
+t/hardware_status.t
diff --git a/FS/t/hardware_class.t b/FS/t/hardware_class.t
new file mode 100644
index 000000000..8c98234ab
--- /dev/null
+++ b/FS/t/hardware_class.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::hardware_class;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/hardware_status.t b/FS/t/hardware_status.t
new file mode 100644
index 000000000..71b3077d5
--- /dev/null
+++ b/FS/t/hardware_status.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::hardware_status;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/hardware_type.t b/FS/t/hardware_type.t
new file mode 100644
index 000000000..072ed7ccd
--- /dev/null
+++ b/FS/t/hardware_type.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::hardware_type;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_dish.t b/FS/t/svc_dish.t
new file mode 100644
index 000000000..684837bc3
--- /dev/null
+++ b/FS/t/svc_dish.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_dish;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_hardware.t b/FS/t/svc_hardware.t
new file mode 100644
index 000000000..83ca81622
--- /dev/null
+++ b/FS/t/svc_hardware.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_hardware;
+$loaded=1;
+print "ok 1\n";