svc_broadband merge
authorkhoff <khoff>
Mon, 9 Sep 2002 23:05:30 +0000 (23:05 +0000)
committerkhoff <khoff>
Mon, 9 Sep 2002 23:05:30 +0000 (23:05 +0000)
25 files changed:
FS/FS/ac.pm [new file with mode: 0644]
FS/FS/ac_block.pm [new file with mode: 0755]
FS/FS/ac_field.pm [new file with mode: 0755]
FS/FS/ac_type.pm [new file with mode: 0755]
FS/FS/cust_svc.pm
FS/FS/part_ac_field.pm [new file with mode: 0755]
FS/FS/part_export.pm
FS/FS/svc_broadband.pm [new file with mode: 0755]
bin/fs-setup
htetc/global.asa
httemplate/browse/ac.cgi [new file with mode: 0755]
httemplate/browse/ac_type.cgi [new file with mode: 0755]
httemplate/edit/ac.cgi [new file with mode: 0755]
httemplate/edit/ac_type.cgi [new file with mode: 0755]
httemplate/edit/part_svc.cgi
httemplate/edit/process/ac.cgi [new file with mode: 0755]
httemplate/edit/process/ac_block.cgi [new file with mode: 0755]
httemplate/edit/process/ac_field.cgi [new file with mode: 0755]
httemplate/edit/process/ac_type.cgi [new file with mode: 0755]
httemplate/edit/process/part_ac_field.cgi [new file with mode: 0755]
httemplate/edit/process/part_svc.cgi
httemplate/edit/process/svc_broadband.cgi [new file with mode: 0644]
httemplate/edit/svc_broadband.cgi [new file with mode: 0644]
httemplate/index.html
httemplate/view/svc_broadband.cgi [new file with mode: 0644]

diff --git a/FS/FS/ac.pm b/FS/FS/ac.pm
new file mode 100644 (file)
index 0000000..5a2b360
--- /dev/null
@@ -0,0 +1,148 @@
+package FS::ac;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs qsearch );
+use FS::ac_type;
+use FS::ac_block;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::ac - Object methods for ac records
+
+=head1 SYNOPSIS
+
+  use FS::ac;
+
+  $record = new FS::ac \%hash;
+  $record = new FS::ac { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::ac record describes a broadband Access Concentrator, such as a DSLAM
+or a wireless access point.  FS::ac inherits from FS::Record.  The following 
+fields are currently supported:
+
+narf
+
+=over 4
+
+=item acnum - primary key
+
+=item actypenum - AC type, see L<FS::ac_type>
+
+=item acname - descriptive name for the AC
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see L<"insert">.
+
+=cut
+
+sub table { 'ac'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  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 $error =
+    $self->ut_numbern('acnum')
+    || $self->ut_number('actypenum')
+    || $self->ut_text('acname');
+  return $error if $error;
+
+  return "Unknown actypenum"
+    unless $self->ac_type;
+  '';
+}
+
+=item ac_type
+
+Returns the L<FS::ac_type> object corresponding to this object.
+
+=cut
+
+sub ac_type {
+  my $self = shift;
+  return qsearchs('ac_type', { actypenum => $self->actypenum });
+}
+
+=item ac_block
+
+Returns a list of L<FS::ac_block> objects (address blocks) associated
+with this object.
+
+=cut
+
+sub ac_block {
+  my $self = shift;
+  return qsearch('ac_block', { acnum => $self->acnum });
+}
+
+=item ac_field
+
+Returns a hash of L<FS::ac_field> objects assigned to this object.
+
+=cut
+
+sub ac_field {
+  my $self = shift;
+
+  return qsearch('ac_field', { acnum => $self->acnum });
+}
+
+=back
+
+=head1 VERSION
+
+$Id:
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_broadband>, L<FS::ac>, L<FS::ac_block>, L<FS::ac_field>,  schema.html
+from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/ac_block.pm b/FS/FS/ac_block.pm
new file mode 100755 (executable)
index 0000000..09de6a4
--- /dev/null
@@ -0,0 +1,148 @@
+package FS::ac_block;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs qsearch );
+use FS::ac_type;
+use FS::ac;
+use FS::svc_broadband;
+use NetAddr::IP;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::ac - Object methods for ac records
+
+=head1 SYNOPSIS
+
+  use FS::ac_block;
+
+  $record = new FS::ac_block \%hash;
+  $record = new FS::ac_block { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::ac_block record describes an address block assigned for broadband 
+access.  FS::ac_block inherits from FS::Record.  The following fields are 
+currently supported:
+
+=over 4
+
+=item acnum - the access concentrator (see L<FS::ac_type>) to which this 
+block is assigned.
+
+=item ip_gateway - the gateway address used by customers within this block.  
+This functions as the primary key.
+
+=item ip_netmask - the netmask of the block, expressed as an integer.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see L<"insert">.
+
+=cut
+
+sub table { 'ac_block'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  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 $error =
+    $self->ut_number('acnum')
+    || $self->ut_ip('ip_gateway')
+    || $self->ut_number('ip_netmask')
+  ;
+  return $error if $error;
+
+  return "Unknown acnum"
+    unless $self->ac;
+
+  my $self_addr = new NetAddr::IP ($self->ip_gateway, $self->ip_netmask);
+  return "Cannot parse address: ". $self->ip_gateway . '/' . $self->ip_netmask
+    unless $self_addr;
+
+  my @block = grep {
+    my $block_addr = new NetAddr::IP ($_->ip_gateway, $_->ip_netmask);
+    if($block_addr->contains($self_addr) 
+    or $self_addr->contains($block_addr)) { $_; };
+  } qsearch( 'ac_block', {});
+
+  foreach(@block) {
+    return "Block intersects existing block ".$_->ip_gateway."/".$_->ip_netmask;
+  }
+
+  '';
+}
+
+
+=item ac
+
+Returns the L<FS::ac> object corresponding to this object.
+
+=cut
+
+sub ac {
+  my $self = shift;
+  return qsearchs('ac', { acnum => $self->acnum });
+}
+
+=item svc_broadband
+
+Returns a list of L<FS::svc_broadband> objects associated
+with this object.
+
+=cut
+
+#sub svc_broadband {
+#  my $self = shift;
+#  my @svc = qsearch('svc_broadband', { actypenum => $self->ac->ac_type->actypenum });
+#  return grep { 
+#    my $svc_addr = new NetAddr::IP($_->ip_addr, $_->ip_netmask);
+#    $self_addr->contains($svc_addr);
+#  } @svc;
+#}
+
+=back
+
+=cut
+
+1;
+
diff --git a/FS/FS/ac_field.pm b/FS/FS/ac_field.pm
new file mode 100755 (executable)
index 0000000..f601119
--- /dev/null
@@ -0,0 +1,138 @@
+package FS::ac_field;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+use FS::part_ac_field;
+use FS::ac;
+
+use UNIVERSAL qw( can );
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::ac_field - Object methods for ac_field records
+
+=head1 SYNOPSIS
+
+  use FS::ac_field;
+
+  $record = new FS::ac_field \%hash;
+  $record = new FS::ac_field { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+L<FS::ac_field> contains values of fields defined by L<FS::part_ac_field>
+for an L<FS::ac>.  Values must be of the data type defined by ut_type in
+L<FS::part_ac_field>.
+Supported fields as follows:
+
+=over 4
+
+=item acfieldpart - Type of ac_field as defined by L<FS::part_ac_field>
+
+=item acnum - The L<FS::ac> to which this value belongs.
+
+=item value - The contents of the field.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see L<"insert">.
+
+=cut
+
+sub table { 'ac_field'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  return "acnum must be defined" unless $self->acnum;
+  return "acfieldpart must be defined" unless $self->acfieldpart;
+
+  my $ut_func = $self->can("ut_" . $self->part_ac_field->ut_type);
+  my $error = $self->$ut_func('value');
+
+  return $error if $error;
+
+  ''; #no error
+}
+
+=item part_ac_field
+
+Returns a reference to the L<FS:part_ac_field> that defines this L<FS::ac_field>
+
+=cut
+
+sub part_ac_field {
+  my $self = shift;
+
+  return qsearchs('part_ac_field', { acfieldpart => $self->acfieldpart });
+}
+
+=item ac
+
+Returns a reference to the L<FS::ac> to which this L<FS::ac_field> belongs.
+
+=cut
+
+sub ac {
+  my $self = shift;
+
+  return qsearchs('ac', { acnum => $self->acnum });
+}
+
+=back
+
+=head1 VERSION
+
+$Id: 
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_broadband>, L<FS::ac>, L<FS::ac_block>, L<FS::ac_field>,  schema.html
+from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/ac_type.pm b/FS/FS/ac_type.pm
new file mode 100755 (executable)
index 0000000..e83c5c5
--- /dev/null
@@ -0,0 +1,128 @@
+package FS::ac_type;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+use FS::ac;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::ac_type - Object methods for ac_type records
+
+=head1 SYNOPSIS
+
+  use FS::ac_type;
+
+  $record = new FS::ac_type \%hash;
+  $record = new FS::ac_type { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+L<FS::ac_type> refers to a type of access concentrator.  L<FS::svc_broadband>
+records refer to a specific L<FS::ac_type> limiting the choice of access
+concentrator to one of the chosen type.  This should be set as a fixed
+default in part_svc to prevent provisioning the wrong type of service for
+a given package or service type.  Supported fields as follows:
+
+=over 4
+
+=item actypenum - Primary key.  see L<FS::ac>
+
+=item actypename - Text identifier for access concentrator type.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see L<"insert">.
+
+=cut
+
+sub table { 'ac_type'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  # What do we check?
+
+  ''; #no error
+}
+
+=item ac
+
+Returns a list of all L<FS::ac> records of this type.
+
+=cut
+
+sub ac {
+  my $self = shift;
+
+  return qsearch('ac', { actypenum => $self->actypenum });
+}
+
+=item part_ac_field
+
+Returns a list of all L<FS::part_ac_field> records of this type.
+
+=cut
+
+sub part_ac_field {
+  my $self = shift;
+
+  return qsearch('part_ac_field', { actypenum => $self->actypenum });
+}
+
+=back
+
+=head1 VERSION
+
+$Id: 
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_broadband>, L<FS::ac>, L<FS::ac_block>, L<FS::ac_field>,  schema.html
+from the base documentation.
+
+=cut
+
+1;
+
index c7cc4b3..d54fb2d 100644 (file)
@@ -295,6 +295,8 @@ sub label {
   } elsif ( $svcdb eq 'svc_www' ) {
     my $domain = qsearchs( 'domain_record', { 'recnum' => $svc_x->recnum } );
     $tag = $domain->reczone;
+  } elsif ( $svcdb eq 'svc_broadband' ) {
+    $tag = $svc_x->ip_addr . '/' . $svc_x->ip_netmask;
   } else {
     cluck "warning: asked for label of unsupported svcdb; using svcnum";
     $tag = $svc_x->getfield('svcnum');
@@ -344,7 +346,7 @@ sub seconds_since {
 
 =head1 VERSION
 
-$Id: cust_svc.pm,v 1.15 2002-05-22 12:17:06 ivan Exp $
+$Id: cust_svc.pm,v 1.16 2002-09-09 23:01:35 khoff Exp $
 
 =head1 BUGS
 
diff --git a/FS/FS/part_ac_field.pm b/FS/FS/part_ac_field.pm
new file mode 100755 (executable)
index 0000000..dcb4452
--- /dev/null
@@ -0,0 +1,102 @@
+package FS::part_ac_field;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+use FS::ac_field;
+use FS::ac;
+
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::part_ac_field - Object methods for part_ac_field records
+
+=head1 SYNOPSIS
+
+  use FS::part_ac_field;
+
+  $record = new FS::part_ac_field \%hash;
+  $record = new FS::part_ac_field { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+
+=over 4
+
+=item blank
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see L<"insert">.
+
+=cut
+
+sub table { 'part_ac_field'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  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 $error = '';
+
+  $self->name =~ /^([a-z0-9_\-\.]{1,15})$/i
+    or return "Invalid field name for part_ac_field";
+
+  ''; #no error
+}
+
+
+=back
+
+=head1 VERSION
+
+$Id: 
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_broadband>, L<FS::ac>, L<FS::ac_block>, L<FS::ac_field>,  schema.html
+from the base documentation.
+
+=cut
+
+1;
+
index bc6a4d7..69cd805 100644 (file)
@@ -839,6 +839,9 @@ tie my %sqlmail_options, 'Tie::IxHash',
 
   },
 
+  'svc_broadband' => {
+  },
+
 );
 
 =back
diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm
new file mode 100755 (executable)
index 0000000..ab92fb3
--- /dev/null
@@ -0,0 +1,295 @@
+package FS::svc_broadband;
+
+use strict;
+use vars qw(@ISA $conf);
+#use FS::Record qw( qsearch qsearchs );
+use FS::Record qw( qsearchs qsearch dbh );
+use FS::svc_Common;
+use FS::cust_svc;
+use NetAddr::IP;
+
+@ISA = qw( FS::svc_Common );
+
+$FS::UID::callback{'FS::svc_broadband'} = sub { 
+  $conf = new FS::Conf;
+};
+
+=head1 NAME
+
+FS::svc_broadband - Object methods for svc_broadband records
+
+=head1 SYNOPSIS
+
+  use FS::svc_broadband;
+
+  $record = new FS::svc_broadband \%hash;
+  $record = new FS::svc_broadband { '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
+
+An FS::svc_broadband object represents a 'broadband' Internet connection, such
+as a DSL, cable modem, or fixed wireless link.  These services are assumed to
+have the following properties:
+
+=over 2
+
+=item
+The network consists of one or more 'Access Concentrators' (ACs), such as
+DSLAMs or wireless access points.  (See L<FS::ac>.)
+
+=item
+Each AC provides connectivity to one or more contiguous blocks of IP addresses,
+each described by a gateway address and a netmask.  (See L<FS::ac_block>.)
+
+=item
+Each connection has one or more static IP addresses within one of these blocks.
+
+=item
+The details of configuring routers and other devices are to be handled by a 
+site-specific L<FS::part_export> subclass.
+
+=back
+
+FS::svc_broadband inherits from FS::svc_Common.  The following fields are
+currently supported:
+
+=over 4
+
+=item svcnum - primary key
+
+=item
+actypenum - access concentrator type; see L<FS::ac_type>.  This is included here
+so that a part_svc can specifically be a 'wireless' or 'DSL' service by
+designating actypenum as a fixed field.  It does create a redundant functional
+dependency between this table and ac_type, in that the matching ac_type could
+be found by looking up the IP address in ac_block and then finding the block's
+AC, but part_svc can't do that, and we don't feel like hacking it so that it
+can.
+
+=item
+speed_up - maximum upload speed, in bits per second.  If set to zero, upload
+speed will be unlimited.  Exports that do traffic shaping should handle this
+correctly, and not blindly set the upload speed to zero and kill the customer's
+connection.
+
+=item
+speed_down - maximum download speed, as above
+
+=item
+ip_addr - the customer's IP address.  If the customer needs more than one IP
+address, set this to the address of the customer's router.  As a result, the
+customer's router will have the same address for both it's internal and external
+interfaces thus saving address space.  This has been found to work on most NAT
+routers available.
+
+=item
+ip_netmask - the customer's netmask, as a single integer in the range 0-32.
+(E.g. '24', not '255.255.255.0'.  We assume that address blocks are contiguous.)
+This should be 32 unless the customer has multiple IP addresses.
+
+=item
+mac_addr - the MAC address of the customer's router or other device directly
+connected to the network, if needed.  Some systems (e.g. DHCP, MAC address-based
+access control) may need this.  If not, you may leave it blank.
+
+=item
+location - a human-readable description of the location of the connected site,
+such as its address.  This should not be used for billing or contact purposes;
+that information is stored in L<FS::cust_main>.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new svc_broadband.  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
+
+sub table { 'svc_broadband'; }
+
+=item insert
+
+Adds this record 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.
+
+=cut
+
+# sub insert {}
+# Standard FS::svc_Common::insert
+# (any necessary Deep Magic is handled by exports)
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# Standard FS::svc_Common::delete
+
+=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
+
+# Standard FS::svc_Common::replace
+# Notice a pattern here?
+
+=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 broadband 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_foreign_key('actypenum', 'ac_type', 'actypenum')
+    || $self->ut_number('speed_up')
+    || $self->ut_number('speed_down')
+    || $self->ut_ip('ip_addr')
+    || $self->ut_numbern('ip_netmask')
+    || $self->ut_textn('mac_addr')
+    || $self->ut_textn('location')
+  ;
+  return $error if $error;
+
+  if($self->speed_up < 0) { return 'speed_up must be positive'; }
+  if($self->speed_down < 0) { return 'speed_down must be positive'; }
+
+  # This should catch errors in the ip_addr and ip_netmask.  If it doesn't,
+  # they'll almost certainly not map into a valid block anyway.
+  my $self_addr = new NetAddr::IP ($self->ip_addr, $self->ip_netmask);
+  return 'Cannot parse address: ' . $self->ip_addr . '/' . $self->ip_netmask unless $self_addr;
+
+  my @block = grep { 
+    my $block_addr = new NetAddr::IP ($_->ip_gateway, $_->ip_netmask);
+    if ($block_addr->contains($self_addr)) { $_ };
+  } qsearch( 'ac_block', { acnum => $self->acnum });
+
+  if(scalar @block == 0) {
+    return 'Block not found for address '.$self->ip_addr.' in actype '.$self->actypenum;
+  } elsif(scalar @block > 1) {
+    return 'ERROR: Intersecting blocks found for address '.$self->ip_addr.' :'.
+        join ', ', map {$_->ip_addr . '/' . $_->ip_netmask} @block;
+  }
+  # OK, we've found a valid block.  We don't actually _do_ anything with it, though; we 
+  # just take comfort in the knowledge that it exists.
+
+  # A simple qsearchs won't work here.  Since we can assign blocks to customers,
+  # we have to make sure the new address doesn't fall within someone else's
+  # block.  Ugh.
+
+  my @conflicts = grep {
+    my $cust_addr = new NetAddr::IP($_->ip_addr, $_->ip_netmask);
+    if (($cust_addr->contains($self_addr)) and
+        ($_->svcnum ne $self->svcnum)) { $_; };
+  } qsearch('svc_broadband', {});
+
+  if (scalar @conflicts > 0) {
+    return 'Address in use by existing service';
+  }
+
+  # Are we trying to use a network, broadcast, or the AC's address?
+  foreach (qsearch('ac_block', { acnum => $self->acnum })) {
+    my $block_addr = new NetAddr::IP($_->ip_gateway, $_->ip_netmask);
+    if ($block_addr->network->addr eq $self_addr->addr) {
+      return 'Address is network address for block '. $block_addr->network;
+    }
+    if ($block_addr->broadcast->addr eq $self_addr->addr) {
+      return 'Address is broadcast address for block '. $block_addr->network;
+    }
+    if ($block_addr->addr eq $self_addr->addr) {
+      return 'Address belongs to the access concentrator: '. $block_addr->addr;
+    }
+  }
+
+  ''; #no error
+}
+
+=item ac_block
+
+Returns the FS::ac_block record (i.e. the address block) for this broadband service.
+
+=cut
+
+sub ac_block {
+  my $self = shift;
+  my $self_addr = new NetAddr::IP ($self->ip_addr, $self->ip_netmask);
+
+  foreach my $block (qsearch( 'ac_block', {} )) {
+    my $block_addr = new NetAddr::IP ($block->ip_addr, $block->ip_netmask);
+    if($block_addr->contains($self_addr)) { return $block; }
+  }
+  return '';
+}
+
+=item ac_type
+
+Returns the FS::ac_type record for this broadband service.
+
+=cut
+
+sub ac_type {
+  my $self = shift;
+  return qsearchs('ac_type', { actypenum => $self->actypenum });
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_Common>, L<FS::Record>, L<FS::ac_type>, L<FS::ac_block>,
+L<FS::part_svc>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index 9522ce3..81f1c26 100755 (executable)
@@ -1,6 +1,6 @@
 #!/usr/bin/perl -Tw
 #
-# $Id: fs-setup,v 1.96 2002-07-06 12:13:49 ivan Exp $
+# $Id: fs-setup,v 1.97 2002-09-09 23:01:36 khoff Exp $
 
 #to delay loading dbdef until we're ready
 BEGIN { $FS::Record::setup_hack = 1; }
@@ -1023,7 +1023,77 @@ sub tables_hash_hack {
       'index'       => [],
     },
 
+    'ac_type' => {
+      'columns' => [
+        'actypenum', 'int', '', '',
+        'actypename', 'varchar', '', 15,
+      ],
+      'primary_key' => 'actypenum',
+      'unique'      => [],
+      'index'       => [],
+    },
+
+    'ac' => {
+      'columns' => [
+        'acnum', 'int', '', '',
+        'actypenum', 'int', '', '',
+        'acname', 'varchar', '', 15,
+      ],
+      'primary_key' => 'acnum',
+      'unique'      => [],
+      'index'       => [],
+    },
+
+    'part_ac_field' => {
+      'columns' => [
+        'acfieldpart', 'int', '', '',
+        'actypenum', 'int', '', '',
+        'name', 'varchar', '', 15,
+        'ut_type', 'varchar', '', 15,
+      ],
+      'primary_key' => 'acfieldpart',
+      'unique'      => [],
+      'index'       => [],
+    },
+
+    'ac_field' => {
+      'columns' => [
+        'acfieldpart', 'int', '', '',
+        'acnum', 'int', '', '',
+        'value', 'varchar', '', 127,
+      ],
+      'primary_key' => '',
+      'unique'      => [ [ 'acfieldpart', 'acnum' ] ],
+      'index'       => [],
+    },
+
+    'ac_block' => {
+      'columns' => [
+        'acnum', 'int', '', '',
+        'ip_gateway', 'char', '', 15,
+        'ip_netmask', 'int', '', '',
+      ],
+      'primary_key' => '',
+      'unique'      => [],
+      'index'       => [],
+    },
 
+    'svc_broadband' => {
+      'columns' => [
+        'svcnum', 'int', '', '',
+        'actypenum', 'int', '', '',
+        'speed_up', 'int', '', '',
+        'speed_down', 'int', '', '',
+        'acnum', 'int', '', '',
+        'ip_addr', 'char', '', 15,
+        'ip_netmask', 'int', '', '',
+        'mac_addr', 'char', '', 17,
+        'location', 'varchar', '', 127,
+      ],
+      'primary_key' => 'svcnum',
+      'unique'      => [],
+      'index'       => [],
+    },
 
   );
 
index d04a5ed..3c8380f 100644 (file)
@@ -46,6 +46,12 @@ use FS::svc_acct_sm;
 use FS::svc_domain;
 use FS::svc_forward;
 use FS::svc_www;
+use FS::ac_type;
+use FS::ac;
+use FS::part_ac_field;
+use FS::ac_field;
+use FS::ac_block;
+use FS::svc_broadband;
 use FS::type_pkgs;
 use FS::part_export;
 use FS::part_export_option;
diff --git a/httemplate/browse/ac.cgi b/httemplate/browse/ac.cgi
new file mode 100755 (executable)
index 0000000..0ae138d
--- /dev/null
@@ -0,0 +1,57 @@
+<!-- mason kludge -->
+<%= header('Access Concentrator Listing', menubar(
+  'Main Menu'   => $p,
+  'Access Concentrator Types' => $p. 'browse/ac_type.cgi',
+)) %>
+<BR>
+<A HREF="<%= $p %>edit/ac.cgi"><I>Add a new Access Concentrator</I></A><BR><BR>
+
+<%= table() %>
+<TR>
+  <TH COLSPAN=2>AC</TH>
+  <TH>AC Type</TH>
+  <TH>Fields</TH>
+  <TH>Network/Mask</TH>
+</TR>
+<% 
+
+foreach my $ac ( qsearch('ac',{}) ) {
+  my($hashref)=$ac->hashref;
+  my($actypenum)=$hashref->{actypenum};
+  my($ac_type)=qsearchs('ac_type',{'actypenum'=>$actypenum});
+  my($actypename)=$ac_type->getfield('actypename');
+  print <<END;
+      <TR>
+        <TD><A HREF="${p}edit/ac.cgi?$hashref->{acnum}">
+          $hashref->{acnum}</A></TD>
+        <TD><A HREF="${p}edit/ac.cgi?$hashref->{acnum}">
+          $hashref->{acname}</A></TD>
+        <TD><A HREF="${p}edit/ac_type.cgi?$actypenum">$actypename</A></TD>
+        <TD>
+END
+
+  foreach my $ac_field ( qsearch('ac_field', { acnum => $hashref->{acnum} }) ) {
+    my $part_ac_field = qsearchs('part_ac_field',
+                         { acfieldpart => $ac_field->getfield('acfieldpart') });
+    print $part_ac_field->getfield('name') . ' ';
+    print $ac_field->getfield('value') . '<BR>';
+  }
+  print '</TD><TD>';
+
+  foreach (qsearch('ac_block', { acnum => $hashref->{acnum} })) {
+    my $net_addr = new NetAddr::IP($_->getfield('ip_gateway'),
+                                   $_->getfield('ip_netmask'));
+    print $net_addr->network->addr . '/' . $net_addr->network->mask . '<BR>';
+  }
+
+  print "<TR>\n";
+
+}
+
+print <<END;
+    </TABLE>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/browse/ac_type.cgi b/httemplate/browse/ac_type.cgi
new file mode 100755 (executable)
index 0000000..0ad8271
--- /dev/null
@@ -0,0 +1,47 @@
+<!-- mason kludge -->
+<%
+
+print header('Access Concentrator Types', menubar(
+  'Main Menu' => $p,
+  'Access Concentrators' => $p. 'browse/ac.cgi',
+)) %>
+<BR>
+<A HREF="<%= $p %>edit/ac_type.cgi"><I>Add new AC Type</I></A><BR><BR>
+<%= table() %>
+      <TR>
+        <TH></TH>
+        <TH>Type</TH>
+        <TH>Fields</TH>
+      </TR>
+
+<%
+foreach my $ac_type ( qsearch('ac_type',{}) ) {
+  my($hashref)=$ac_type->hashref;
+  print <<END;
+      <TR>
+        <TD><A HREF="${p}edit/ac_type.cgi?$hashref->{actypenum}">
+          $hashref->{actypenum}</A></TD>
+        <TD><A HREF="${p}edit/ac_type.cgi?$hashref->{actypenum}">
+          $hashref->{actypename}</A></TD>
+        <TD>
+END
+
+  foreach ( qsearch('part_ac_field', { actypenum => $hashref->{actypenum} }) ) {
+    my ($part_ac_field) = $_->hashref;
+    print $part_ac_field->{'name'} .
+          ' (' . $part_ac_field->{'ut_type'} . ')<BR>';
+  }
+
+}
+
+print <<END;
+       </TD>
+      </TR>
+      <TR>
+      </TR>
+    </TABLE>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/ac.cgi b/httemplate/edit/ac.cgi
new file mode 100755 (executable)
index 0000000..86b05a4
--- /dev/null
@@ -0,0 +1,163 @@
+<!-- mason kludge -->
+<%
+
+my($ac);
+if ( $cgi->param('error') ) {
+  $ac = new FS::ac ( {
+    map { $_, scalar($cgi->param($_)) } fields('ac')
+  } );
+} elsif ( $cgi->keywords ) { #editing
+  my( $query ) = $cgi->keywords;
+  $query =~ /^(\d+)$/;
+  $ac=qsearchs('ac',{'acnum'=>$1});
+} else { #adding
+  $ac = new FS::ac {};
+}
+my $action = $ac->acnum ? 'Edit' : 'Add';
+my $hashref = $ac->hashref;
+
+print header("$action Access Concentrator", menubar(
+  'Main Menu' => "$p",
+  'View all access concentrators' => "${p}browse/ac.cgi",
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print '<FORM ACTION="', popurl(1), 'process/ac.cgi" METHOD=POST>',
+      qq!<INPUT TYPE="hidden" NAME="acnum" VALUE="$hashref->{acnum}">!,
+      "Access Concentrator #", $hashref->{acnum} ? $hashref->{acnum} : "(NEW)";
+
+print <<END;
+
+<TABLE COLOR="#cccccc">
+  <TR>
+    <TH ALIGN="RIGHT">Access Concentrator</TH>
+    <TD>
+      <INPUT TYPE="text" NAME="acname" SIZE=15 VALUE="$hashref->{acname}">
+    </TD>
+  </TD>
+END
+
+
+if (! $ac->acnum) {
+  print <<END;
+  <TR>
+    <TH ALIGN="RIGHT">Access Concentrator Type</TH>
+    <TD><SELECT NAME="actypenum" SIZE="1"><OPTION VALUE=""></OPTION>
+END
+
+  foreach (qsearch('ac_type', {})) {
+    my $narf = $_->hashref;
+    print qq!<OPTION! .
+          ($narf->{actypenum} eq $hashref->{actypenum} ? ' SELECTED' : '') .
+          qq! VALUE="$narf->{actypenum}">$narf->{actypenum}: $narf->{actypename}! .
+          qq!</OPTION>!;
+  }
+
+  print '</TD></TR></TABLE>';
+} else {
+  print '</TABLE>';
+  print qq!<INPUT TYPE="hidden" NAME="actypenum" VALUE="$hashref->{actypenum}">!;
+}
+
+print qq!</TABLE><BR><BR><INPUT TYPE="submit" VALUE="!,
+      $hashref->{acnum} ? "Apply changes" : "Add access concentrator",
+      qq!"></FORM>!;
+
+if ($hashref->{acnum}) {
+  print table();
+  print <<END;
+    Additional Fields:<BR>
+    <TH>
+      <TD>Field Name</TD>
+      <TD COLSPAN=2>Field Value</TD>
+    </TH>
+END
+
+  #my @ac_fields = qsearch('ac_field', { acnum => $hashref->{acnum} });
+  my @ac_fields = $ac->ac_field;
+  foreach (@ac_fields) {
+    print qq!\n<TR><TD></TD>!;
+    my $part_ac_field = qsearchs('part_ac_field',
+                          { acfieldpart => $_->getfield('acfieldpart') });
+    print '<TD>' . $part_ac_field->getfield('name') .
+          '</TD><TD>' . $_->getfield('value') . '</TD></TR>';
+    print "\n";
+  }
+
+  print '<FORM ACTION="', popurl(1), 'process/ac_field.cgi" METHOD=POST>';
+  print <<END;
+    <TR>
+      <TD><INPUT TYPE="hidden" NAME="acnum" VALUE="$hashref->{acnum}">
+          <INPUT TYPE="hidden" NAME="acname" VALUE="$hashref->{acname}">
+          <INPuT TYPE="hidden" NAME="actypenum" VALUE="$hashref->{actypenum}">
+          <SMALL>(NEW)</SMALL>
+      </TD>
+      <TD><SELECT NAME="acfieldpart"><OPTION></OPTION>
+END
+
+  my @part_ac_fields = qsearch('part_ac_field',
+                         { actypenum => $hashref->{actypenum} });
+  foreach my $part_ac_field (@part_ac_fields) {
+    my $acfieldpart = $part_ac_field->getfield('acfieldpart');
+    if (grep {$_->getfield('acfieldpart') eq $acfieldpart} @ac_fields) {next;}
+    print qq!<OPTION VALUE="${acfieldpart}">! .
+          $part_ac_field->getfield('name') . '</OPTION>';
+  }
+
+  print <<END;
+        </SELECT>
+      </TD>
+      <TD><INPUT TYPE="text" SIZE="15" NAME="value"></TD>
+      <TD><INPUT TYPE="submit" VALUE="Add"></TD>
+    </TR>
+    </FORM>
+  </TABLE>
+END
+
+}
+
+if ($hashref->{acnum}) {
+
+  print qq!<BR><BR>IP Address Blocks:<BR>! . table() .
+        qq!<TR><TH></TH><TH>Network/Mask</TH>! .
+        qq!<TH>Gateway Address</TH><TH>Mask length</TH></TR>\n!;
+
+  foreach (qsearch('ac_block', { acnum => $hashref->{acnum} })) {
+    my $ip_addr = new NetAddr::IP($_->getfield('ip_gateway'),
+                                  $_->getfield('ip_netmask'));
+    print qq!<TR><TD></TD><TD>! . $ip_addr->network->addr() . '/' .
+          $ip_addr->network->mask() . qq!</TD>!;
+
+    print qq!<TD>! . $_->getfield('ip_gateway') . qq!</TD>\n! .
+          qq!<TD>! . $_->getfield('ip_netmask') . qq!</TD></TR>!;
+
+  }
+
+  print '<FORM ACTION="', popurl(1), 'process/ac_block.cgi" METHOD=POST>';
+  print <<END;
+  <TR>
+    <TD><INPUT TYPE="hidden" NAME="acnum" VALUE="$hashref->{acnum}">
+        <INPUT TYPE="hidden" NAME="acname" VALUE="$hashref->{acname}">
+        <INPuT TYPE="hidden" NAME="actypenum" VALUE="$hashref->{actypenum}">
+       <SMALL>(NEW)</SMALL>
+    </TD>
+    <TD></TD>
+    <TD><INPUT TYPE="text" NAME="ip_gateway" SIZE="15"></TD>
+    <TD><INPUT TYPE="text" NAME="ip_netmask" SIZE="2"></TD>
+    <TD><INPUT TYPE="submit" VALUE="Add"></TD>
+  </TR>
+  </FORM>
+</TABLE>
+END
+
+}
+
+print <<END;
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/ac_type.cgi b/httemplate/edit/ac_type.cgi
new file mode 100755 (executable)
index 0000000..ccc3d57
--- /dev/null
@@ -0,0 +1,106 @@
+<!-- mason kludge -->
+<%
+
+my $ac_type;
+if ( $cgi->param('error') ) {
+  $ac_type = new FS::ac_type ( {
+    map { $_, scalar($cgi->param($_)) } fields('ac_type')
+  } );
+} elsif ( $cgi->keywords ) { #editing
+  my($query)=$cgi->keywords;
+  $query =~ /^(\d+)$/;
+  $ac_type=qsearchs('ac_type',{'actypenum'=>$1});
+} else { #adding
+  $ac_type = new FS::ac_type {};
+}
+my $action = $ac_type->actypenum ? 'Edit' : 'Add';
+my $hashref = $ac_type->hashref;
+
+my @ut_types = qw( float number text alpha anything ip domain );
+
+my $p1 = popurl(1);
+print header("$action Access Concentrator Type", menubar(
+  'Main Menu' => popurl(2),
+  'View all Access Concentrator types' => popurl(2). "browse/ac_type.cgi",
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print qq!<FORM ACTION="${p1}process/ac_type.cgi" METHOD=POST>!;
+
+#display
+
+print qq!<INPUT TYPE="hidden" NAME="actypenum" VALUE="$hashref->{actypenum}">!,
+      "AC Type #", $hashref->{actypenum} ? $hashref->{actypenum} : "(NEW)";
+
+print <<TROZ;
+<PRE>
+AC Type Name <INPUT TYPE="text" NAME="actypename" SIZE=15 VALUE="$hashref->{actypename}">
+</PRE>
+
+TROZ
+
+print qq!<BR><INPUT TYPE="submit" VALUE="!,
+      $hashref->{actypenum} ? "Apply changes" : "Add AC Type",
+      qq!"></FORM>!;
+
+
+if ($hashref->{actypenum}) {
+  print qq!   <BR>Available fields:<BR>! .  table();
+
+  print qq!    <TH><TD>Field name</TD><TD>Field type</TD><TD></TD></TH>!;
+
+  my @part_ac_field = qsearch ( 'part_ac_field',
+                                { actypenum => $hashref->{actypenum} } );
+  foreach ( @part_ac_field ) {
+    my $pf_hashref = $_->hashref;
+    print <<END;
+      <TR>
+        <TD>$pf_hashref->{acfieldpart}</TD>
+        <TD>$pf_hashref->{name}</TD>
+        <TD>$pf_hashref->{ut_type}</TD>
+      </TR>
+END
+  }
+
+  my $name, $ut_type = '';
+  if ($cgi->param('error')) {
+    $name = $cgi->param('name');
+    $ut_type = $cgi->param('ut_type');
+  }
+
+  print <<END;
+      <FORM ACTION="${p1}process/part_ac_field.cgi" METHOD=GET>
+      <TR>
+       <TD><SMALL>(NEW)</SMALL>
+         <INPUT TYPE="hidden" NAME="actypenum" VALUE="$hashref->{actypenum}">
+       </TD>
+       <TD>
+         <INPUT TYPE="text" NAME="name" VALUE="${name}">
+       </TD>
+       <TD>
+         <SELECT NAME="ut_type" SIZE=1><OPTION>
+END
+
+  foreach ( @ut_types ) {
+    print qq!<OPTION! . ($ut_type ? " SELECTED>$_" : ">$_");
+  }
+
+  print <<END;
+        </SELECT>
+      </TD>
+      <TD><INPUT TYPE="submit" VALUE="Add"></TD>
+    </TR>
+    </FORM>
+  </TABLE>
+END
+
+}
+
+%>
+
+ </BODY>
+</HTML>
+
index 4ccb770..a23107a 100755 (executable)
@@ -53,6 +53,7 @@ Services are items you offer to your customers.
     <LI>svc_acct_sm - <B>deprecated</B> (use svc_forward for new installations) Virtual domain mail aliasing.
     <LI>svc_forward - mail forwarding
     <LI>svc_www - Virtual domain website
+    <LI>svc_broadband - Broadband/High-speed Internet service
 <!--   <LI>svc_charge - One-time charges (Partially unimplemented)
        <LI>svc_wo - Work orders (Partially unimplemented)
 -->
@@ -122,11 +123,21 @@ my %defs = (
     #'recnum' => '',
     #'usersvc' => '',
   },
+  'svc_broadband' => {
+    'actypenum' => 'This is the actypenum that refers to the type of AC that can be provisioned for this service.  This field must be set fixed.',
+    '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.',
+    'acnum' => 'acnum of a specific AC that this service is restricted to.  Not required',
+    'ip_addr' => 'IP address.  Leave blank for automatic assignment.',
+    'ip_netmask' => 'Mask length, aka. netmask bits.  (Eg. 255.255.255.0 == 24)',
+    'mac_addr' => 'MAC address which is used by some ACs for access control.  Specified by 6 colon seperated hex octets. (Eg. 00:00:0a:bc:1a:2b)',
+    'location' => 'Defines the physically location at which this service was installed.  This is not necessarily the billing address',
+  },
 );
 
   my @dbs = $hashref->{svcdb}
              ? ( $hashref->{svcdb} )
-             : qw( svc_acct svc_domain svc_acct_sm svc_forward svc_www );
+             : qw( svc_acct svc_domain svc_acct_sm svc_forward svc_www svc_broadband );
 
   tie my %svcdb, 'Tie::IxHash', map { $_=>$_ } @dbs;
   my $widget = new HTML::Widgets::SelectLayers(
diff --git a/httemplate/edit/process/ac.cgi b/httemplate/edit/process/ac.cgi
new file mode 100755 (executable)
index 0000000..fc434a8
--- /dev/null
@@ -0,0 +1,28 @@
+<%
+
+my $acnum = $cgi->param('acnum');
+
+my $old = qsearchs('ac',{'acnum'=>$acnum}) if $acnum;
+
+my $new = new FS::ac ( {
+  map {
+    $_, scalar($cgi->param($_));
+  } fields('ac')
+} );
+
+my $error = '';
+if ( $acnum ) {
+  $error = $new->replace($old);
+} else {
+  $error = $new->insert;
+  $acnum=$new->getfield('acnum');
+}
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "ac.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "browse/ac.cgi");
+}
+
+%>
diff --git a/httemplate/edit/process/ac_block.cgi b/httemplate/edit/process/ac_block.cgi
new file mode 100755 (executable)
index 0000000..b1c3c72
--- /dev/null
@@ -0,0 +1,21 @@
+<%
+
+my $new = new FS::ac_block ( {
+  map {
+    $_, scalar($cgi->param($_));
+  } fields('ac_block')
+} );
+
+my $error = '';
+$error = $new->check;
+
+unless ( $error ) { $error = $new->insert; }
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "ac.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(2). "ac.cgi?". $cgi->param('acnum'));
+}
+
+%>
diff --git a/httemplate/edit/process/ac_field.cgi b/httemplate/edit/process/ac_field.cgi
new file mode 100755 (executable)
index 0000000..2bfe331
--- /dev/null
@@ -0,0 +1,21 @@
+<%
+
+my $new = new FS::ac_field ( {
+  map {
+    $_, scalar($cgi->param($_));
+  } fields('ac_field')
+} );
+
+my $error = '';
+$error = $new->check;
+
+unless ( $error ) { $error = $new->insert; }
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "ac.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(2). "ac.cgi?". $cgi->param('acnum'));
+}
+
+%>
diff --git a/httemplate/edit/process/ac_type.cgi b/httemplate/edit/process/ac_type.cgi
new file mode 100755 (executable)
index 0000000..ca232ba
--- /dev/null
@@ -0,0 +1,28 @@
+<%
+
+my $actypenum = $cgi->param('actypenum');
+
+my $old = qsearchs('ac_type',{'actypenum'=>$actypenum}) if $actypenum;
+
+my $new = new FS::ac_type ( {
+  map {
+    $_, scalar($cgi->param($_));
+  } fields('ac_type')
+} );
+
+my $error = '';
+if ( $actypenum ) {
+  $error = $new->replace($old);
+} else {
+  $error = $new->insert;
+  $actypenum=$new->getfield('actypenum');
+}
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "ac_type.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "browse/ac_type.cgi");
+}
+
+%>
diff --git a/httemplate/edit/process/part_ac_field.cgi b/httemplate/edit/process/part_ac_field.cgi
new file mode 100755 (executable)
index 0000000..38ad586
--- /dev/null
@@ -0,0 +1,21 @@
+<%
+
+my $new = new FS::part_ac_field ( {
+  map {
+    $_, scalar($cgi->param($_));
+  } fields('part_ac_field')
+} );
+
+my $error = '';
+$error = $new->check;
+
+unless ( $error ) { $error = $new->insert; }
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "ac_type.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(2). "ac_type.cgi?". $cgi->param('actypenum'));
+}
+
+%>
index 859670b..69e8ac2 100755 (executable)
@@ -17,7 +17,7 @@ my $new = new FS::part_svc ( {
             push @fields, 'usergroup' if $svcdb eq 'svc_acct'; #kludge
             map { ( $svcdb.'__'.$_, $svcdb.'__'.$_.'_flag' )  } @fields;
           } grep defined( $FS::Record::dbdef->table($_) ),
-                 qw( svc_acct svc_domain svc_acct_sm svc_forward svc_www )
+                 qw( svc_acct svc_domain svc_acct_sm svc_forward svc_www svc_broadband )
     )
 } );
 
diff --git a/httemplate/edit/process/svc_broadband.cgi b/httemplate/edit/process/svc_broadband.cgi
new file mode 100644 (file)
index 0000000..fd7ba20
--- /dev/null
@@ -0,0 +1,45 @@
+<%
+
+$cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
+my $svcnum = $1;
+
+my $old;
+if ( $svcnum ) {
+  $old = qsearchs('svc_broadband', { 'svcnum' => $svcnum } )
+    or die "fatal: can't find broadband service (svcnum $svcnum)!";
+} else {
+  $old = '';
+}
+
+my $new = new FS::svc_broadband ( {
+  map {
+    ($_, scalar($cgi->param($_)));
+  } ( fields('svc_broadband'), qw( pkgnum svcpart ) )
+} );
+
+unless ( $new->ip_addr ) {
+  $new->ip_addr(join('.', (map $cgi->param('ip_addr_'.$_), (a..d))));
+}
+
+unless ( $new->mac_addr) {
+  $new->mac_addr(join(':', (map $cgi->param('mac_addr_'.$_), (a..f))));
+}
+
+my $error;
+if ( $svcnum ) {
+  $error = $new->replace($old);
+} else {
+  $error = $new->insert;
+  $svcnum = $new->svcnum;
+}
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  $cgi->param('ip_addr', $new->ip_addr);
+  $cgi->param('mac_addr', $new->mac_addr);
+  print $cgi->redirect(popurl(2). "svc_broadband.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "view/svc_broadband.cgi?" . $svcnum );
+}
+
+%>
diff --git a/httemplate/edit/svc_broadband.cgi b/httemplate/edit/svc_broadband.cgi
new file mode 100644 (file)
index 0000000..d8a1f7a
--- /dev/null
@@ -0,0 +1,219 @@
+<!-- mason kludge -->
+<%
+
+my( $svcnum,  $pkgnum, $svcpart, $part_svc, $svc_broadband );
+if ( $cgi->param('error') ) {
+  $svc_broadband = new FS::svc_broadband ( {
+    map { $_, scalar($cgi->param($_)) } fields('svc_broadband')
+  } );
+  $svcnum = $svc_broadband->svcnum;
+  $pkgnum = $cgi->param('pkgnum');
+  $svcpart = $cgi->param('svcpart');
+  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+  die "No part_svc entry!" unless $part_svc;
+} else {
+  my($query) = $cgi->keywords;
+  if ( $query =~ /^(\d+)$/ ) { #editing
+    $svcnum=$1;
+    $svc_broadband=qsearchs('svc_broadband',{'svcnum'=>$svcnum})
+      or die "Unknown (svc_broadband) svcnum!";
+
+    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+      or die "Unknown (cust_svc) svcnum!";
+
+    $pkgnum=$cust_svc->pkgnum;
+    $svcpart=$cust_svc->svcpart;
+  
+    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+    die "No part_svc entry!" unless $part_svc;
+
+  } else { #adding
+
+    $svc_broadband = new FS::svc_broadband({});
+
+    foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
+      $pkgnum=$1 if /^pkgnum(\d+)$/;
+      $svcpart=$1 if /^svcpart(\d+)$/;
+    }
+    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+    die "No part_svc entry!" unless $part_svc;
+
+    $svcnum='';
+
+    #set fixed and default fields from part_svc
+    foreach my $part_svc_column (
+      grep { $_->columnflag } $part_svc->all_part_svc_column
+    ) {
+      $svc_broadband->setfield( $part_svc_column->columnname,
+                                $part_svc_column->columnvalue,
+                              );
+    }
+
+  }
+}
+my $action = $svc_broadband->svcnum ? 'Edit' : 'Add';
+
+my @ac_list;
+
+if ($pkgnum) {
+
+  unless ($svc_broadband->actypenum) {die "actypenum must be set fixed";};
+  @ac_list = qsearch('ac', { actypenum => $svc_broadband->getfield('actypenum') });
+
+} elsif ( $action eq 'Edit' ) {
+
+  #Nothing?
+
+} else {
+  die "\$action eq Add, but \$pkgnum is null!\n";
+}
+
+
+my $p1 = popurl(1);
+print header("Broadband Service $action", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print qq!<FORM ACTION="${p1}process/svc_broadband.cgi" METHOD=POST>!;
+
+#display
+
+
+#svcnum
+print qq!<INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">!;
+print qq!Service #<B>!, $svcnum ? $svcnum : "(NEW)", "</B><BR><BR>";
+
+#pkgnum
+print qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">!;
+#svcpart
+print qq!<INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">!;
+
+#actypenum
+print '<INPUT TYPE="hidden" NAME="actypenum" VALUE="' .
+      $svc_broadband->actypenum . '">';
+
+
+print &ntable("#cccccc",2) . qq!<TR><TD ALIGN="right">AC</TD><TD>!;
+
+#acnum
+if (( $part_svc->part_svc_column('acnum')->columnflag eq 'F' ) or
+    ( !$pkgnum )) {
+
+  my $ac = qsearchs('ac', { acnum => $svc_broadband->acnum });
+  my ($acnum, $acname) = ($ac->acnum, $ac->acname);
+
+  print qq!<INPUT TYPE="hidden" NAME="acnum" VALUE="${acnum}">! .
+        qq!${acnum}: ${acname}</TD></TR>!;
+
+} else {
+
+  my @ac_list = qsearch('ac', { actypenum => $svc_broadband->actypenum });
+  print qq!<SELECT NAME="acnum" SIZE="1"><OPTION VALUE=""></OPTION>!;
+
+  foreach ( @ac_list ) {
+    my ($acnum, $acname) = ($_->acnum, $_->acname);
+    print qq!<OPTION VALUE="${acnum}"! .
+          ($acnum == $svc_broadband->acnum ? ' SELECTED>' : '>') .
+          qq!${acname}</OPTION>!;
+  }
+  print '</TD></TR>';
+
+}
+
+#speed_up & speed_down
+my ($speed_up, $speed_down) = ($svc_broadband->speed_up,
+                               $svc_broadband->speed_down);
+
+print '<TR><TD ALIGN="right">Download speed</TD><TD>';
+if ( $part_svc->part_svc_column('speed_down')->columnflag eq 'F' ) {
+  print qq!<INPUT TYPE="hidden" NAME="speed_down" VALUE="${speed_down}">! .
+        qq!${speed_down}Kbps</TD></TR>!;
+} else {
+  print qq!<INPUT TYPE="text" NAME="speed_down" SIZE=5 VALUE="${speed_down}">! .
+        qq!Kbps</TD></TR>!;
+}
+
+print '<TR><TD ALIGN="right">Upload speed</TD><TD>';
+if ( $part_svc->part_svc_column('speed_up')->columnflag eq 'F' ) {
+  print qq!<INPUT TYPE="hidden" NAME="speed_up" VALUE="${speed_up}">! .
+        qq!${speed_up}Kbps</TD></TR>!;
+} else {
+  print qq!<INPUT TYPE="text" NAME="speed_up" SIZE=5 VALUE="${speed_up}">! .
+        qq!Kbps</TD></TR>!;
+}
+
+#ip_addr & ip_netmask
+#We're assuming that ip_netmask is fixed if ip_addr is fixed.
+#If it isn't, well, <shudder> what the heck are you doing!?!?
+
+my ($ip_addr, $ip_netmask) = ($svc_broadband->ip_addr,
+                              $svc_broadband->ip_netmask);
+
+print '<TR><TD ALIGN="right">IP address/Mask</TD><TD>';
+if ( $part_svc->part_svc_column('ip_addr')->columnflag eq 'F' ) {
+  print qq!<INPUT TYPE="hidden" NAME="ip_addr" VALUE="${ip_addr}">! .
+        qq!<INPUT TYPE="hidden" NAME="ip_netmask" VALUE="${ip_netmask}">! .
+        qq!${ip_addr}/${ip_netmask}</TD></TR>!;
+} else {
+  $ip_addr =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
+  print <<END;
+  <INPUT TYPE="text" NAME="ip_addr_a" SIZE="3" MAXLENGTH="3" VALUE="${1}">.
+  <INPUT TYPE="text" NAME="ip_addr_b" SIZE="3" MAXLENGTH="3" VALUE="${2}">.
+  <INPUT TYPE="text" NAME="ip_addr_c" SIZE="3" MAXLENGTH="3" VALUE="${3}">.
+  <INPUT TYPE="text" NAME="ip_addr_d" SIZE="3" MAXLENGTH="3" VALUE="${4}">/
+  <INPUT TYPE="text" NAME="ip_netmask" SIZE="2" MAXLENGTH="2" VALUE="${ip_netmask}">
+</TD></TR>
+<TR><TD COLSPAN="2" WIDTH="300">
+<P><SMALL>Leave the IP address and netmask blank for automatic assignment of a /32 address.  Specifing the netmask and not the address will force assignment of a larger block.</SMALL></P>
+</TD></TR>
+END
+}
+
+#mac_addr
+my $mac_addr = $svc_broadband->mac_addr;
+
+unless (( $part_svc->part_svc_column('mac_addr')->columnflag eq 'F' ) and
+        ( $mac_addr eq '' )) {
+  print '<TR><TD ALIGN="right">MAC Address</TD><TD>';
+  if ( $part_svc->part_svc_column('mac_addr')->columnflag eq 'F' ) { #Why?
+    print qq!<INPUT TYPE="hidden" NAME="mac_addr" VALUE="${mac_addr}">! .
+          qq!${mac_addr}</TD></TR>!;
+  } else {
+    #Ewwww
+    $mac_addr =~ /^([a-f0-9]{2}):([a-f0-9]{2}):([a-f0-9]{2}):([a-f0-9]{2}):([a-f0-9]{2}):([a-f0-9]{2})$/i;
+    print <<END;
+  <INPUT TYPE="text" NAME="mac_addr_a" SIZE="2" MACLENGTH="2" VALUE="${1}">:
+  <INPUT TYPE="text" NAME="mac_addr_b" SIZE="2" MACLENGTH="2" VALUE="${2}">:
+  <INPUT TYPE="text" NAME="mac_addr_c" SIZE="2" MACLENGTH="2" VALUE="${3}">:
+  <INPUT TYPE="text" NAME="mac_addr_d" SIZE="2" MACLENGTH="2" VALUE="${4}">:
+  <INPUT TYPE="text" NAME="mac_addr_e" SIZE="2" MACLENGTH="2" VALUE="${5}">:
+  <INPUT TYPE="text" NAME="mac_addr_f" SIZE="2" MACLENGTH="2" VALUE="${6}">
+</TD></TR>
+END
+
+  }
+}
+
+#location
+my $location = $svc_broadband->location;
+
+print '<TR><TD VALIGN="top" ALIGN="right">Location</TD><TD BGCOLOR="#e8e8e8">';
+if ( $part_svc->part_svc_column('location')->columnflag eq 'F' ) {
+  print qq!<PRE>${location}</PRE></TD></TR>!;
+} else {
+  print qq!<TEXTAREA ROWS="4" COLS="30" NAME="location">${location}</TEXTAREA>!;
+}
+
+print '</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">';
+
+print <<END;
+
+    </FORM>
+  </BODY>
+</HTML>
+END
+%>
index dce020b..2cb7073 100644 (file)
               into counties and assign different tax rates to each.
           <LI><A HREF="browse/svc_acct_pop.cgi">View/Edit Access Numbers</A>
             - Points of Presence 
+          <LI><A HREF="browse/ac_type.cgi">View/Edit AC Types</A>
+            - Broadband service access concentrator types.
+          <LI><A HREF="browse/ac.cgi">View/Edit AC</A>
+            - Broadband service access concentrators.
           <LI><A HREF="browse/part_bill_event.cgi">View/Edit invoice events</A> - Actions for overdue invoices
           <LI><A HREF="browse/msgcat.cgi">View/Edit message catalog</A> - Change error messages and other customizable labels.
         </ul>
diff --git a/httemplate/view/svc_broadband.cgi b/httemplate/view/svc_broadband.cgi
new file mode 100644 (file)
index 0000000..156edfa
--- /dev/null
@@ -0,0 +1,75 @@
+<!-- mason kludge -->
+<%
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_broadband = qsearchs( 'svc_broadband', { 'svcnum' => $svcnum } )
+  or die "svc_broadband: Unknown svcnum $svcnum";
+
+#false laziness w/all svc_*.cgi
+my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $svcnum } );
+my $pkgnum = $cust_svc->getfield('pkgnum');
+my($cust_pkg, $custnum);
+if ($pkgnum) {
+  $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $pkgnum } );
+  $custnum = $cust_pkg->custnum;
+} else {
+  $cust_pkg = '';
+  $custnum = '';
+}
+#eofalse
+
+my $ac = qsearchs('ac', { acnum => $svc_broadband->getfield('acnum') });
+
+my (
+     $acname,
+     $acnum,
+     $speed_down,
+     $speed_up,
+     $ip_addr,
+     $ip_netmask,
+     $mac_addr,
+     $location
+   ) = (
+     $ac->getfield('acname'),
+     $ac->getfield('acnum'),
+     $svc_broadband->getfield('speed_down'),
+     $svc_broadband->getfield('speed_up'),
+     $svc_broadband->getfield('ip_addr'),
+     $svc_broadband->getfield('ip_netmask'),
+     $svc_broadband->getfield('mac_addr'),
+     $svc_broadband->getfield('location')
+   );
+
+print header('Broadband Service View', menubar(
+  ( ( $custnum )
+    ? ( "View this package (#$pkgnum)" => "${p}view/cust_pkg.cgi?$pkgnum",
+        "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+      )                                                                       
+    : ( "Cancel this (unaudited) website" =>
+          "${p}misc/cancel-unaudited.cgi?$svcnum" )
+  ),
+  "Main menu" => $p,
+)).
+      qq!<A HREF="${p}edit/svc_broadband.cgi?$svcnum">Edit this information</A><BR>!.
+      ntable("#cccccc"). '<TR><TD>'. ntable("#cccccc",2).
+      qq!<TR><TD ALIGN="right">Service number</TD>!.
+        qq!<TD BGCOLOR="#ffffff">$svcnum</TD></TR>!.
+      qq!<TR><TD ALIGN="right">AC</TD>!.
+        qq!<TD BGCOLOR="#ffffff">$acnum: $acname</TD></TR>!.
+      qq!<TR><TD ALIGN="right">Download Speed</TD>!.
+        qq!<TD BGCOLOR="#ffffff">$speed_down</TD></TR>!.
+      qq!<TR><TD ALIGN="right">Upload Speed</TD>!.
+        qq!<TD BGCOLOR="#ffffff">$speed_up</TD></TR>!.
+      qq!<TR><TD ALIGN="right">IP Address/Mask</TD>!.
+        qq!<TD BGCOLOR="#ffffff">$ip_addr/$ip_netmask</TD></TR>!.
+      qq!<TR><TD ALIGN="right">MAC Address</TD>!.
+        qq!<TD BGCOLOR="#ffffff">$mac_addr</TD></TR>!.
+      qq!<TR><TD ALIGN="right" VALIGN="TOP">Location</TD>!.
+        qq!<TD BGCOLOR="#ffffff"><PRE>$location</PRE></TD></TR>!.
+      '</TABLE></TD></TR></TABLE>'.
+      '<BR>'. joblisting({'svcnum'=>$svcnum}, 1).
+      '</BODY></HTML>'
+;
+%>