svc_broadband rewrite
authorkhoff <khoff>
Wed, 5 Feb 2003 23:17:17 +0000 (23:17 +0000)
committerkhoff <khoff>
Wed, 5 Feb 2003 23:17:17 +0000 (23:17 +0000)
28 files changed:
FS/FS/addr_block.pm [new file with mode: 0755]
FS/FS/part_router_field.pm [new file with mode: 0755]
FS/FS/part_sb_field.pm [new file with mode: 0755]
FS/FS/part_svc_router.pm [new file with mode: 0755]
FS/FS/router.pm [new file with mode: 0755]
FS/FS/router_field.pm [new file with mode: 0755]
FS/FS/sb_field.pm [new file with mode: 0755]
FS/FS/svc_broadband.pm
FS/bin/freeside-setup
htetc/global.asa
htetc/handler.pl
httemplate/browse/addr_block.cgi [new file with mode: 0644]
httemplate/browse/generic.cgi [new file with mode: 0644]
httemplate/browse/part_sb_field.cgi [new file with mode: 0644]
httemplate/browse/router.cgi [new file with mode: 0644]
httemplate/edit/part_router_field.cgi [new file with mode: 0644]
httemplate/edit/part_sb_field.cgi [new file with mode: 0644]
httemplate/edit/process/addr_block/add.cgi [new file with mode: 0755]
httemplate/edit/process/addr_block/allocate.cgi [new file with mode: 0755]
httemplate/edit/process/addr_block/deallocate.cgi [new file with mode: 0755]
httemplate/edit/process/addr_block/split.cgi [new file with mode: 0755]
httemplate/edit/process/generic.cgi [new file with mode: 0644]
httemplate/edit/process/router.cgi [new file with mode: 0644]
httemplate/edit/process/svc_broadband.cgi
httemplate/edit/router.cgi [new file with mode: 0755]
httemplate/edit/svc_broadband.cgi
httemplate/index.html
httemplate/view/svc_broadband.cgi

diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm
new file mode 100755 (executable)
index 0000000..b671723
--- /dev/null
@@ -0,0 +1,322 @@
+package FS::addr_block;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs qsearch dbh );
+use FS::router;
+use FS::svc_broadband;
+use NetAddr::IP;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::addr_block - Object methods for addr_block records
+
+=head1 SYNOPSIS
+
+  use FS::addr_block;
+
+  $record = new FS::addr_block \%hash;
+  $record = new FS::addr_block { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::addr_block record describes an address block assigned for broadband 
+access.  FS::addr_block inherits from FS::Record.  The following fields are 
+currently supported:
+
+=over 4
+
+=item blocknum - primary key, used in FS::svc_broadband to associate 
+services to the block.
+
+=item routernum - the router (see FS::router) to which this 
+block is assigned.
+
+=item ip_gateway - the gateway address used by customers within this block.  
+
+=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 "insert".
+
+=cut
+
+sub table { 'addr_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.
+
+sub delete {
+  my $self = shift;
+  return 'Block must be deallocated before deletion'
+    if $self->router;
+
+  $self->SUPER::delete;
+}
+
+=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('routernum')
+    || $self->ut_ip('ip_gateway')
+    || $self->ut_number('ip_netmask')
+  ;
+  return $error if $error;
+
+
+  # A routernum of 0 indicates an unassigned block and is allowed
+  return "Unknown routernum"
+    if ($self->routernum and not $self->router);
+
+  my $self_addr = $self->NetAddr;
+  return "Cannot parse address: ". $self->ip_gateway . '/' . $self->ip_netmask
+    unless $self_addr;
+
+  if (not $self->blocknum) {
+    my @block = grep {
+      my $block_addr = $_->NetAddr;
+      if($block_addr->contains($self_addr) 
+      or $self_addr->contains($block_addr)) { $_; };
+    } qsearch( 'addr_block', {});
+    foreach(@block) {
+      return "Block intersects existing block ".$_->ip_gateway."/".$_->ip_netmask;
+    }
+  }
+
+  '';
+}
+
+
+=item router
+
+Returns the FS::router object corresponding to this object.  If the 
+block is unassigned, returns undef.
+
+=cut
+
+sub router {
+  my $self = shift;
+  return qsearchs('router', { routernum => $self->routernum });
+}
+
+=item svc_broadband
+
+Returns a list of FS::svc_broadband objects associated
+with this object.
+
+=cut
+
+sub svc_broadband {
+  my $self = shift;
+  return qsearch('svc_broadband', { blocknum => $self->blocknum });
+}
+
+=item NetAddr
+
+Returns a NetAddr::IP object for this block's address and netmask.
+
+=cut
+
+sub NetAddr {
+  my $self = shift;
+
+  return new NetAddr::IP ($self->ip_gateway, $self->ip_netmask);
+}
+
+=item next_free_addr
+
+Returns a NetAddr::IP object corresponding to the first unassigned address 
+in the block (other than the network, broadcast, or gateway address).  If 
+there are no free addresses, returns false.
+
+=cut
+
+sub next_free_addr {
+  my $self = shift;
+
+  my @used = map { $_->NetAddr->addr } 
+      ($self, 
+       qsearch('svc_broadband', { blocknum => $self->blocknum }) );
+
+  my @free = $self->NetAddr->hostenum;
+  while (my $ip = shift @free) {
+    if (not grep {$_ eq $ip->addr;} @used) { return $ip; };
+  }
+
+  '';
+
+}
+
+=item allocate
+
+Allocates this address block to a router.  Takes an FS::router object 
+as an argument.
+
+At present it's not possible to reallocate a block to a different router 
+except by deallocating it first, which requires that none of its addresses 
+be assigned.  This is probably as it should be.
+
+=cut
+
+sub allocate {
+  my ($self, $router) = @_;
+
+  return 'Block is already allocated'
+    if($self->router);
+
+  return 'Block must be allocated to a router'
+    unless(ref $router eq 'FS::router');
+
+  my @svc = $self->svc_broadband;
+  if (@svc) {
+    return 'Block has assigned addresses: '. join ', ', map {$_->ip_addr} @svc;
+  }
+
+  my $new = new FS::addr_block {$self->hash};
+  $new->routernum($router->routernum);
+  return $new->replace($self);
+
+}
+
+=item deallocate
+
+Deallocates the block (i.e. sets the routernum to 0).  If any addresses in the 
+block are assigned to services, it fails.
+
+=cut
+
+sub deallocate {
+  my $self = shift;
+
+  my @svc = $self->svc_broadband;
+  if (@svc) {
+    return 'Block has assigned addresses: '. join ', ', map {$_->ip_addr} @svc;
+  }
+
+  my $new = new FS::addr_block {$self->hash};
+  $new->routernum(0);
+  return $new->replace($self);
+}
+
+=item split_block
+
+Splits this address block into two equal blocks, occupying the same space as
+the original block.  The first of the two will also have the same blocknum.
+The gateway address of each block will be set to the first usable address, i.e.
+(network address)+1.  Since this method is designed for use on unallocated
+blocks, this is probably the correct behavior.
+
+(At present, splitting allocated blocks is disallowed.  Anyone who wants to
+implement this is reminded that each split costs three addresses, and any
+customers who were using these addresses will have to be moved; depending on
+how full the block was before being split, they might have to be moved to a
+different block.  Anyone who I<still> wants to implement it is asked to tie it
+to a configuration switch so that site admins can disallow it.)
+
+=cut
+
+sub split_block {
+
+  # We should consider using Attribute::Handlers/Aspect/Hook::LexWrap/
+  # something to atomicize functions, so that we can say 
+  #
+  # sub split_block : atomic {
+  # 
+  # instead of repeating all this AutoCommit verbage in every 
+  # sub that does more than one database operation.
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $self = shift;
+  my $error;
+
+  if ($self->router) {
+    return 'Block is already allocated';
+  }
+
+  #TODO: Smallest allowed block should be a config option.
+  if ($self->NetAddr->masklen() ge 30) {
+    return 'Cannot split blocks with a mask length >= 30';
+  }
+
+  my (@new, @ip);
+  $ip[0] = $self->NetAddr;
+  @ip = map {$_->first()} $ip[0]->split($self->ip_netmask + 1);
+
+  foreach (0,1) {
+    $new[$_] = new FS::addr_block {$self->hash};
+    $new[$_]->ip_gateway($ip[$_]->addr);
+    $new[$_]->ip_netmask($ip[$_]->masklen);
+  }
+
+  $new[1]->blocknum('');
+
+  $error = $new[0]->replace($self);
+  if ($error) {
+    $dbh->rollback;
+    return $error;
+  }
+
+  $error = $new[1]->insert;
+  if ($error) {
+    $dbh->rollback;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  return '';
+}
+
+=item merge
+
+To be implemented.
+
+=back
+
+=head1 BUGS
+
+Minimum block size should be a config option.  It's hardcoded at /30 right
+now because that's the smallest block that makes any sense at all.
+
+1;
+
diff --git a/FS/FS/part_router_field.pm b/FS/FS/part_router_field.pm
new file mode 100755 (executable)
index 0000000..73ca50f
--- /dev/null
@@ -0,0 +1,134 @@
+package FS::part_router_field;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+use FS::router_field;
+use FS::router;
+
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::part_router_field - Object methods for part_router_field records
+
+=head1 SYNOPSIS
+
+  use FS::part_router_field;
+
+  $record = new FS::part_router_field \%hash;
+  $record = new FS::part_router_field { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+A part_router_field represents an xfield definition for routers.  For more
+information on xfields, see L<FS::part_sb_field>.
+
+The following fields are supported:
+
+=over 4
+
+=item routerfieldpart - primary key (assigned automatically)
+
+=item name - name of field
+
+=item length
+
+=item check_block
+
+=item list_source
+
+(See L<FS::part_sb_field> for details on these fields.)
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see "insert".
+
+=cut
+
+sub table { 'part_router_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_router_field";
+
+  ''; #no error
+}
+
+=item list_values
+
+Equivalent to "eval($part_router_field->list_source)".
+
+=cut
+
+sub list_values {
+  my $self = shift;
+  return () unless $self->list_source;
+  my @opts = eval($self->list_source);
+  if($@) { 
+    warn $@;
+    return ();
+  } else { 
+    return @opts;
+  }
+}
+
+=back
+
+=head1 VERSION
+
+$Id: 
+
+=head1 BUGS
+
+Needless duplication of much of FS::part_sb_field, with the result that most of
+the warnings about it apply here also.
+
+=head1 SEE ALSO
+
+FS::svc_broadband, FS::router, FS::router_field,  schema.html
+from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_sb_field.pm b/FS/FS/part_sb_field.pm
new file mode 100755 (executable)
index 0000000..8dca946
--- /dev/null
@@ -0,0 +1,267 @@
+package FS::part_sb_field;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::part_sb_field - Object methods for part_sb_field records
+
+=head1 SYNOPSIS
+
+  use FS::part_sb_field;
+
+  $record = new FS::part_sb_field \%hash;
+  $record = new FS::part_sb_field { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_sb_field object represents an extended field (xfield) definition 
+for svc_broadband's sb_field mechanism (see L<FS::svc_broadband>).  
+FS::part_sb_field inherits from FS::Record.  The following fields are 
+currently supported:
+
+=over 2
+
+=item sbfieldpart - primary key (assigned automatically)
+
+=item name - name of the field
+
+=item svcpart - service type for which this field is available (see L<FS::part_svc>)
+
+=item length - length of the contents of the field (see note #1)
+
+=item check_block - validation routine (see note #2)
+
+=item list_source - enumeration routine (see note #3)
+
+=back
+
+=head1 BACKGROUND
+
+Broadband services, unlike dialup services, are provided over a wide 
+variety of physical media (DSL, wireless, cable modems, digital circuits) 
+and network architectures (Ethernet, PPP, ATM).  For many of these access 
+mechanisms, adding a new customer requires knowledge of some properties 
+of the physical connection (circuit number, the type of CPE in use, etc.).
+It is unreasonable to expect ISPs to alter Freeside's schema (and the 
+associated library and UI code) to make each of these parameters a field in 
+svc_broadband.
+
+Hence sb_field and part_sb_field.  They allow the Freeside administrator to
+define 'extended fields' ('xfields') associated with svc_broadband records.
+These are I<not> processed in any way by Freeside itself; they exist solely for
+use by exports (see L<FS::part_export>) and technical support staff.
+
+For a parallel mechanism (at the per-router level rather than per-service), 
+see L<FS::part_router_field>.
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see "insert".
+
+=cut
+
+sub table { 'part_sb_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 = '';
+
+  $error = $self->ut_numbern('svcpart');
+  return $error if $error;
+
+  unless (qsearchs('part_svc', { svcpart => $self->svcpart }))
+    { return "Unknown svcpart: " . $self->svcpart;}
+
+  $self->name =~ /^([a-z0-9_\-\.]{1,15})$/i
+    or return "Invalid field name for part_sb_field";
+
+  #How to check input_block, display_block, and check_block?
+
+  ''; #no error
+}
+
+=item list_values
+
+If the I<list_source> field is set, this method eval()s it and 
+returns its output.  If the field is empty, list_values returns 
+an empty list.
+
+Any arguments passed to this method will be received by the list_source 
+code, but this behavior is a fortuitous accident and may be removed in 
+the future.
+
+=cut
+
+sub list_values {
+  my $self = shift;
+  return () unless $self->list_source;
+
+  my @opts = eval($self->list_source);
+  if($@) {
+    warn $@;
+    return ();
+  } else {
+    return @opts;
+  }
+}
+
+=item part_svc
+
+Returns the FS::part_svc object associated with this field definition.
+
+=cut
+
+sub part_svc {
+  my $self = shift;
+  return qsearchs('part_svc', { svcpart => $self->svcpart });
+}
+
+=back
+
+=head1 VERSION
+
+$Id: 
+
+=head1 NOTES
+
+=over
+
+=item 1.
+
+The I<length> field is not enforced.  It provides a hint to UI
+code about how to display the field on a form.  If you want to enforce a
+minimum or maximum length for a field, use a I<check_block>.
+
+=item 2.
+
+The check_block mechanism used here as well as in
+FS::part_router_field allows the user to define validation rules.
+
+When FS::sb_field::check is called, the proposed value of the xfield is
+assigned to $_.  The check_block is then eval()'d and its return value
+captured.  If the return value is false (empty/zero/undef), $_ is then assigned
+back into the field and stored in the database.
+
+Therefore a check_block can do three different things with the value: allow
+it, allow it with a modification, or reject it.  This is very flexible, but
+somewhat dangerous.  Some warnings:
+
+=over 2
+
+=item *
+
+Assume that $_ has had I<no> error checking prior to the
+check_block.  That's what the check_block is for, after all.  It could
+contain I<anything>: evil shell commands in backquotes, 100kb JPEG images,
+the Klez virus, whatever.
+
+=item *
+
+If your check_block modifies the input value, it should probably
+produce a value that wouldn't be modified by going through the same
+check_block again.  (That is, it should map input values into its own
+eigenspace.)  The reason is that if someone calls $new->replace($old),
+where $new and $old contain the same value for the field, they probably
+want the field to keep its old value, not to get transformed by the
+check_block again.  So don't do silly things like '$_++' or
+'tr/A-Za-z/a-zA-Z/'.
+
+=item *
+
+Don't alter the contents of the database.  I<Reading> the database
+is perfectly reasonable, but writing to it is a bad idea.  Remember that
+check() might get called more than once, as described above.
+
+=item *
+
+The check_block probably won't even get called if the user submits
+an I<empty> sb_field.  So at present, you can't set up a default value with
+something like 's/^$/foo/'.  Conversely, don't replace the submitted value
+with an empty string.  It probably will get stored, but might be deleted at
+any time.
+
+=back
+
+=item 3.
+
+The list_source mechanism is a UI hint (like length) to generate
+drop-down or list boxes.  If list_source contains a value, the UI code can
+eval() it and use the results as the options on the list.
+
+Note 'can'.  This is not a substitute for check_block.  The HTML interface
+currently requires that the user pick one of the options on the list
+because that's the way HTML drop-down boxes work, but in the future the UI
+code might add an 'Other (please specify)' option and a text box so that
+the user can enter something else.  Or it might ignore list_source and just
+generate a text box.  Or the interface might be rewritten in MS Access,
+where drop-down boxes have text boxes built in.  Data validation is the job
+of check(), not the front end.
+
+Note also that a list of literals evaluates to itself, so a list_source
+like
+
+C<('Windows', 'MacOS', 'Linux')>
+
+or
+
+C<qw(Windows MacOS Linux)>
+
+means exactly what you'd think.
+
+=head1 BUGS
+
+The lack of any way to do default values.  We might add this as another UI
+hint (since, for the most part, it's the UI's job to figure out which fields
+have had values entered into them).  In fact, there are lots of things we
+should add as UI hints.
+
+Oh, and the documentation is probably full of lies.
+
+=head1 SEE ALSO
+
+FS::svc_broadband, FS::sb_field, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_svc_router.pm b/FS/FS/part_svc_router.pm
new file mode 100755 (executable)
index 0000000..0b23ab5
--- /dev/null
@@ -0,0 +1,32 @@
+package FS::part_svc_router;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw(qsearchs);
+use FS::router;
+use FS::part_svc;
+
+@ISA = qw(FS::Record);
+
+sub table { 'part_svc_router'; }
+
+sub check {
+  my $self = shift;
+  my $error =
+    $self->ut_foreign_key('svcpart', 'part_svc', 'svcpart')
+    || $self->ut_foreign_key('routernum', 'router', 'routernum');
+  return $error if $error;
+  ''; #no error
+}
+
+sub router {
+  my $self = shift;
+  return qsearchs('router', { routernum => $self->routernum });
+}
+
+sub part_svc {
+  my $self = shift;
+  return qsearchs('part_svc', { svcpart => $self->svcpart });
+}
+
+1;
diff --git a/FS/FS/router.pm b/FS/FS/router.pm
new file mode 100755 (executable)
index 0000000..3f9459a
--- /dev/null
@@ -0,0 +1,156 @@
+package FS::router;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs qsearch );
+use FS::addr_block;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::router - Object methods for router records
+
+=head1 SYNOPSIS
+
+  use FS::router;
+
+  $record = new FS::router \%hash;
+  $record = new FS::router { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::router record describes a broadband router, such as a DSLAM or a wireless
+ access point.  FS::router inherits from FS::Record.  The following 
+fields are currently supported:
+
+=over 4
+
+=item routernum - primary key
+
+=item routername - descriptive name for the router
+
+=item svcnum - svcnum of the owning FS::svc_broadband, if appropriate
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see "insert".
+
+=cut
+
+sub table { 'router'; }
+
+=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('routernum')
+    || $self->ut_text('routername');
+  return $error if $error;
+
+  '';
+}
+
+=item addr_block
+
+Returns a list of FS::addr_block objects (address blocks) associated
+with this object.
+
+=cut
+
+sub addr_block {
+  my $self = shift;
+  return qsearch('addr_block', { routernum => $self->routernum });
+}
+
+=item router_field
+
+Returns a list of FS::router_field objects assigned to this object.
+
+=cut
+
+sub router_field {
+  my $self = shift;
+
+  return qsearch('router_field', { routernum => $self->routernum });
+}
+
+=item part_svc_router
+
+Returns a list of FS::part_svc_router objects associated with this 
+object.  This is unlikely to be useful for any purpose other than retrieving 
+the associated FS::part_svc objects.  See below.
+
+=cut
+
+sub part_svc_router {
+  my $self = shift;
+  return qsearch('part_svc_router', { routernum => $self->routernum });
+}
+
+=item part_svc
+
+Returns a list of FS::part_svc objects associated with this object.
+
+=cut
+
+sub part_svc {
+  my $self = shift;
+  return map { qsearchs('part_svc', { svcpart => $_->svcpart }) }
+      $self->part_svc_router;
+}
+
+=back
+
+=head1 VERSION
+
+$Id:
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+FS::svc_broadband, FS::router, FS::addr_block, FS::router_field, FS::part_svc,
+schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/router_field.pm b/FS/FS/router_field.pm
new file mode 100755 (executable)
index 0000000..eee21ab
--- /dev/null
@@ -0,0 +1,146 @@
+package FS::router_field;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+use FS::part_router_field;
+use FS::router;
+
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::router_field - Object methods for router_field records
+
+=head1 SYNOPSIS
+
+  use FS::router_field;
+
+  $record = new FS::router_field \%hash;
+  $record = new FS::router_field { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+FS::router_field contains values of router xfields.  See FS::part_sb_field 
+for details on the xfield mechanism.
+
+=over 4
+
+=item routerfieldpart - Type of router_field as defined by 
+FS::part_router_field
+
+=item routernum - The FS::router 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 "insert".
+
+=cut
+
+sub table { 'router_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 "routernum must be defined" unless $self->routernum;
+  return "routerfieldpart must be defined" unless $self->routerfieldpart;
+
+  my $part_router_field = $self->part_router_field;
+  $_ = $self->value;
+
+  my $check_block = $part_router_field->check_block;
+  if ($check_block) {
+    $@ = '';
+    my $error = (eval($check_block) or $@);
+    return $error if $error;
+    $self->setfield('value' => $_);
+  }
+
+  ''; #no error
+}
+
+=item part_router_field
+
+Returns a reference to the FS:part_router_field that defines this 
+FS::router_field
+
+=cut
+
+sub part_router_field {
+  my $self = shift;
+
+  return qsearchs('part_router_field', 
+    { routerfieldpart => $self->routerfieldpart });
+}
+
+=item router
+
+Returns a reference to the FS::router to which this FS::router_field 
+belongs.
+
+=cut
+
+sub router {
+  my $self = shift;
+
+  return qsearchs('router', { routernum => $self->routernum });
+}
+
+=back
+
+=head1 VERSION
+
+$Id: 
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+FS::svc_broadband, FS::router, FS::router_block, FS::router_field,  
+schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/sb_field.pm b/FS/FS/sb_field.pm
new file mode 100755 (executable)
index 0000000..d4eb378
--- /dev/null
@@ -0,0 +1,148 @@
+package FS::sb_field;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+use FS::part_sb_field;
+
+use UNIVERSAL qw( can );
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::sb_field - Object methods for sb_field records
+
+=head1 SYNOPSIS
+
+  use FS::sb_field;
+
+  $record = new FS::sb_field \%hash;
+  $record = new FS::sb_field { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+See L<FS::part_sb_field> for details on this table's mission in life.
+FS::sb_field contains the actual values of the xfields defined in
+part_sb_field.
+
+The following fields are supported:
+
+=over 4
+
+=item sbfieldpart - Type of sb_field as defined by FS::part_sb_field
+
+=item svcnum - The svc_broadband 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 { 'sb_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 the value against the check_block of the corresponding part_sb_field.
+Returns whatever the check_block returned (unless the check_block dies, in 
+which case check returns the die message).  Therefore, if the check_block 
+wants to allow the value to be stored, it must return false.  See 
+L<FS::part_sb_field> for details.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  return "svcnum must be defined" unless $self->svcnum;
+  return "sbfieldpart must be defined" unless $self->sbfieldpart;
+
+  my $part_sb_field = $self->part_sb_field;
+
+  $_ = $self->value;
+
+  my $check_block = $self->part_sb_field->check_block;
+  if ($check_block) {
+    $@ = '';
+    my $error = (eval($check_block) or $@); # treat fatal errors as errors
+    return $error if $error;
+    $self->setfield('value' => $_);
+  }
+
+  ''; #no error
+}
+
+=item part_sb_field
+
+Returns a reference to the FS::part_sb_field that defines this FS::sb_field.
+
+=cut
+
+sub part_sb_field {
+  my $self = shift;
+
+  return qsearchs('part_sb_field', { sbfieldpart => $self->sbfieldpart });
+}
+
+=back
+
+=item svc_broadband
+
+Returns a reference to the FS::svc_broadband to which this value is attached.
+Nobody's ever going to use this function, but here it is anyway.
+
+=cut
+
+sub svc_broadband {
+  my $self = shift;
+
+  return qsearchs('svc_broadband', { svcnum => $self->svcnum });
+}
+
+=head1 VERSION
+
+$Id: 
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_broadband>, schema.html
+from the base documentation.
+
+=cut
+
+1;
+
index ab92fb3..45f6c36 100755 (executable)
@@ -2,10 +2,10 @@ 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 FS::addr_block;
 use NetAddr::IP;
 
 @ISA = qw( FS::svc_Common );
@@ -45,25 +45,6 @@ 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:
 
@@ -71,14 +52,7 @@ currently supported:
 
 =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 blocknum - see FS::addr_block
 
 =item
 speed_up - maximum upload speed, in bits per second.  If set to zero, upload
@@ -89,28 +63,12 @@ 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
+=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 its 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
@@ -120,7 +78,7 @@ that information is stored in L<FS::cust_main>.
 =item new HASHREF
 
 Creates a new svc_broadband.  To add the record to the database, see
-L<"insert">.
+"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.
@@ -134,14 +92,12 @@ sub table { 'svc_broadband'; }
 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 
+The additional fields pkgnum and svcpart (see 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
 
@@ -159,19 +115,62 @@ returns the error, otherwise returns false.
 =cut
 
 # Standard FS::svc_Common::replace
-# Notice a pattern here?
+
+=item sb_field
+
+Returns a list of FS::sb_field objects assigned to this object.
+
+=cut
+
+sub sb_field {
+  my $self = shift;
+
+  return qsearch( 'sb_field', { svcnum => $self->svcnum } );
+}
+
+=item sb_field_hashref
+
+Returns a hashref of the FS::sb_field key/value pairs for this object.
+
+Deprecated.  Please don't use it.
+
+=cut
+
+# Kristian wrote this, but don't hold it against him.  He was under a powerful
+# distracting influence whom he evidently found much more interesting than
+# svc_broadband.pm.  I can't say I blame him.
+
+sub sb_field_hashref {
+  my $self = shift;
+  my $svcpart = shift;
+
+  if ((not $svcpart) && ($self->cust_svc)) {
+    $svcpart = $self->cust_svc->svcpart;
+  }
+
+  my $hashref = {};
+
+  map {
+    my $sb_field = qsearchs('sb_field', { sbfieldpart => $_->sbfieldpart,
+                                          svcnum => $self->svcnum });
+    $hashref->{$_->getfield('name')} = $sb_field ? $sb_field->getfield('value') : '';
+  } qsearch('part_sb_field', { svcpart => $svcpart });
+
+  return $hashref;
+
+}
 
 =item suspend
 
-Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
 
 =item unsuspend
 
-Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
 
 =item cancel
 
-Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
+Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
 
 =item check
 
@@ -189,105 +188,99 @@ sub check {
 
   my $error =
     $self->ut_numbern('svcnum')
-    || $self->ut_foreign_key('actypenum', 'ac_type', 'actypenum')
+    || $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum')
     || $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')
+    || $self->ut_ipn('ip_addr')
   ;
   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;
+  if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
+    $self->ip_addr($self->addr_block->next_free_addr->addr);
+    if (not $self->ip_addr) {
+      return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
+    }
   }
-  # 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.
+  # This should catch errors in the ip_addr.  If it doesn't,
+  # they'll almost certainly not map into the block anyway.
+  my $self_addr = $self->NetAddr; #netmask is /32
+  return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
 
-  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';
+  my $block_addr = $self->addr_block->NetAddr;
+  unless ($block_addr->contains($self_addr)) {
+    return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
   }
 
-  # 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;
-    }
+  my $router = $self->addr_block->router 
+    or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
+  if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
+  } # do nothing
+  else {
+    return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
   }
 
+
   ''; #no error
 }
 
-=item ac_block
+=item NetAddr
 
-Returns the FS::ac_block record (i.e. the address block) for this broadband service.
+Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
+is /32.
 
 =cut
 
-sub ac_block {
+sub NetAddr {
   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 '';
+  return new NetAddr::IP ($self->ip_addr);
 }
 
-=item ac_type
+=item addr_block
 
-Returns the FS::ac_type record for this broadband service.
+Returns the FS::addr_block record (i.e. the address block) for this broadband service.
 
 =cut
 
-sub ac_type {
+sub addr_block {
   my $self = shift;
-  return qsearchs('ac_type', { actypenum => $self->actypenum });
+
+  return qsearchs('addr_block', { blocknum => $self->blocknum });
 }
 
 =back
 
+=item allowed_routers
+
+Returns a list of allowed FS::router objects.
+
+=cut
+
+sub allowed_routers {
+  my $self = shift;
+
+  return map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
+}
+
 =head1 BUGS
 
+I think there's one place in the code where we actually use sb_field_hashref.
+That's a bug in itself.
+
+The real problem with it is that we're still grappling with the question of how
+tightly xfields should be integrated with real fields.  There are a few
+different directions we could go with it--we I<could> override several
+functions in Record so that xfields behave almost exactly like real fields (can
+be set with setfield(), appear in fields() and hash(), used as criteria in
+qsearch(), etc.).
+
 =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.
+FS::svc_Common, FS::Record, FS::addr_block, FS::sb_field,
+FS::part_svc, schema.html from the base documentation.
 
 =cut
 
index 0ef3fc8..1948376 100755 (executable)
@@ -1012,76 +1012,99 @@ sub tables_hash_hack {
       'index'       => [],
     },
 
-    'ac_type' => {
+    'router' => {
       'columns' => [
-        'actypenum', 'serial', '', '',
-        'actypename', 'varchar', '', $char_d,
+        'routernum', 'serial', '', '',
+        'routername', 'varchar', '', $char_d,
+        'svcnum', 'int', '0', '',
       ],
-      'primary_key' => 'actypenum',
+      'primary_key' => 'routernum',
       'unique'      => [],
       'index'       => [],
     },
 
-    'ac' => {
+    'part_svc_router' => {
       'columns' => [
-        'acnum', 'serial', '', '',
-        'actypenum', 'int', '', '',
-        'acname', 'varchar', '', $char_d,
-      ],
-      'primary_key' => 'acnum',
+        'svcpart', 'int', '', '',
+       'routernum', 'int', '', '',
+      ];
+      'primary_key' => '',
       'unique'      => [],
-      'index'       => [ [ 'actypenum' ] ],
+      'index'       => [],
     },
 
-    'part_ac_field' => {
+    'part_router_field' => {
       'columns' => [
-        'acfieldpart', 'serial', '', '',
-        'actypenum', 'int', '', '',
+        'routerfieldpart', 'serial', '', '',
         'name', 'varchar', '', $char_d,
-        'ut_type', 'varchar', '', $char_d,
+       'length', 'int', '', '',
+       'check_block', 'text', 'NULL', '',
+       'list_source', 'text', 'NULL', '',
       ],
-      'primary_key' => 'acfieldpart',
+      'primary_key' => 'routerfieldpart',
       'unique'      => [],
-      'index'       => [ [ 'actypenum' ] ],
+      'index'       => [],
     },
 
-    'ac_field' => {
+    'router_field' => {
       'columns' => [
-        'acfieldpart', 'int', '', '',
-        'acnum', 'int', '', '',
-        'value', 'text', '', '',
+        'routerfieldpart', 'int', '', '',
+        'routernum', 'int', '', '',
+        'value', 'varchar', '', 128,
       ],
       'primary_key' => '',
-      'unique'      => [ [ 'acfieldpart', 'acnum' ] ],
-      'index'       => [ [ 'acnum' ] ],
+      'unique'      => [ [ 'routerfieldpart', 'routernum' ] ],
+      'index'       => [],
     },
 
-    'ac_block' => {
+    'addr_block' => {
       'columns' => [
-        'acnum', 'int', '', '',
+        'blocknum', 'int', '', '',
+       'routernum', 'int', '', '',
         'ip_gateway', 'varchar', '', 15,
         'ip_netmask', 'int', '', '',
       ],
+      'primary_key' => 'blocknum',
+      'unique'      => [ [ 'blocknum', 'routernum' ] ],
+      'index'       => [],
+    },
+
+    'part_sb_field' => {
+      'columns' => [
+        'sbfieldpart', 'int', '', '',
+       'svcpart', 'int', '', '',
+       'name', 'varchar', '', $char_d,
+       'length', 'int', '', '',
+       'check_block', 'text', 'NULL', '',
+       'list_source', 'text', 'NULL', '',
+      ],
+      'primary_key' => 'sbfieldpart',
+      'unique'      => [ [ 'sbfieldpart', 'svcpart' ] ],
+      'index'       => [],
+    },
+
+    'sb_field' => {
+      'columns' => [
+        'sbfieldpart', 'int', '', '',
+       'svcnum', 'int', '', '',
+       'value', 'varchar', '', 128,
+      ],
       'primary_key' => '',
-      'unique'      => [],
-      'index'       => [ [ 'acnum' ] ],
+      'unique'      => [ [ 'sbfieldpart', 'svcnum' ] ],
+      'index'       => [],
     },
 
     'svc_broadband' => {
       'columns' => [
         'svcnum', 'int', '', '',
-        'actypenum', 'int', '', '',
+        'blocknum', 'int', '', '',
         'speed_up', 'int', '', '',
         'speed_down', 'int', '', '',
-        'acnum', 'int', '', '',
         'ip_addr', 'varchar', '', 15,
-        'ip_netmask', 'int', '', '',
-        'mac_addr', 'char', '', 17,
-        'location', 'varchar', '', $char_d,
       ],
       'primary_key' => 'svcnum',
       'unique'      => [],
-      'index'       => [ [ 'actypenum' ] ],
+      'index'       => [],
     },
 
   );
index 7d84600..5fd8995 100644 (file)
@@ -41,6 +41,7 @@ use FS::part_bill_event;
 use FS::part_pkg;
 use FS::part_referral;
 use FS::part_svc;
+use FS::part_svc_router;
 use FS::pkg_svc;
 use FS::port;
 use FS::queue qw(joblisting);
@@ -51,11 +52,12 @@ use FS::svc_acct_pop qw(popselector);
 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::router;
+use FS::part_router_field;
+use FS::router_field;
+use FS::addr_block;
+use FS::part_sb_field;
+use FS::sb_field;
 use FS::svc_broadband;
 use FS::type_pkgs;
 use FS::part_export;
index d55ba33..768ebff 100644 (file)
@@ -94,6 +94,7 @@ sub handler
       use FS::part_pkg;
       use FS::part_referral;
       use FS::part_svc;
+      use FS::part_svc_router;
       use FS::pkg_svc;
       use FS::port;
       use FS::queue qw(joblisting);
@@ -105,6 +106,13 @@ sub handler
       use FS::svc_domain;
       use FS::svc_forward;
       use FS::svc_www;
+      use FS::router;
+      use FS::part_router_field;
+      use FS::router_field;
+      use FS::addr_block;
+      use FS::part_sb_field;
+      use FS::sb_field;
+      use FS::svc_broadband;
       use FS::type_pkgs;
       use FS::part_export;
       use FS::part_export_option;
diff --git a/httemplate/browse/addr_block.cgi b/httemplate/browse/addr_block.cgi
new file mode 100644 (file)
index 0000000..06ac556
--- /dev/null
@@ -0,0 +1,76 @@
+<%= header('Address Blocks', menubar('Main Menu'   => $p)) %>
+<%
+
+use NetAddr::IP;
+
+my @addr_block = qsearch('addr_block', {});
+my @router = qsearch('router', {});
+my $block;
+my $p2 = popurl(2);
+my $path = $p2 . "edit/process/addr_block";
+
+%>
+
+<% if ($cgi->param('error')) { %>
+   <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+   <BR><BR>
+<% } %>
+
+<%=table()%>
+
+<% foreach $block (sort {$a->NetAddr cmp $b->NetAddr} @addr_block) { %>
+  <TR>
+    <TD><%=$block->NetAddr%></TD>
+  <% if (my $router = $block->router) { %>
+    <% if (scalar($block->svc_broadband) == 0) { %>
+    <TD>
+      <%=$router->routername%>
+    </TD>
+    <TD>
+      <FORM ACTION="<%=$path%>/deallocate.cgi" METHOD="POST">
+        <INPUT TYPE="hidden" NAME="blocknum" VALUE="<%=$block->blocknum%>">
+        <INPUT TYPE="submit" NAME="submit" VALUE="Deallocate">
+      </FORM>
+    </TD>
+    <% } else { %>
+    <TD COLSPAN="2">
+    <%=$router->routername%>
+    </TD>
+    <% } %>
+  <% } else { %>
+    <TD>
+      <FORM ACTION="<%=$path%>/allocate.cgi" METHOD="POST">
+        <INPUT TYPE="hidden" NAME="blocknum" VALUE="<%=$block->blocknum%>">
+        <SELECT NAME="routernum" SIZE="1">
+    <% foreach (@router) { %>
+          <OPTION VALUE="<%=$_->routernum %>"><%=$_->routername%></OPTION>
+    <% } %>
+        </SELECT>
+        <INPUT TYPE="submit" NAME="submit" VALUE="Allocate">
+      </FORM>
+    </TD>
+    <TD>
+      <FORM ACTION="<%=$path%>/split.cgi" METHOD="POST">
+        <INPUT TYPE="hidden" NAME="blocknum" VALUE="<%=$block->blocknum%>">
+        <INPUT TYPE="submit" NAME="submit" VALUE="Split">
+      </FORM>
+    </TD>
+  </TR>
+<% }
+ } %>
+  <TR><TD COLSPAN="3"><BR></TD></TR>
+  <TR>
+    <FORM ACTION="<%=$path%>/add.cgi" METHOD="POST">
+    <TD>Gateway/Netmask</TD>
+    <TD>
+      <INPUT TYPE="text" NAME="ip_gateway" SIZE="15">/<INPUT TYPE="text" NAME="ip_netmask" SIZE="2">
+    </TD>
+    <TD>
+      <INPUT TYPE="submit" NAME="submit" VALUE="Add">
+    </TD>
+    </FORM>
+  </TR>
+</TABLE>
+</BODY>
+</HTML>
+
diff --git a/httemplate/browse/generic.cgi b/httemplate/browse/generic.cgi
new file mode 100644 (file)
index 0000000..9ac0f23
--- /dev/null
@@ -0,0 +1,46 @@
+<%
+
+use FS::Record qw(qsearch dbdef);
+use DBIx::DBSchema;
+use DBIx::DBSchema::Table;
+
+my $error;
+my $p2 = popurl(2);
+my ($table) = $cgi->keywords;
+my $dbdef = dbdef or die "Cannot fetch dbdef!";
+my $dbdef_table = $dbdef->table($table) or die "Cannot fetch schema for $table";
+
+my $pkey = $dbdef_table->primary_key or die "Cannot fetch pkey for $table";
+print header("Browse $table", menubar('Main Menu'   => $p));
+
+my @rec = qsearch($table, {});
+my @col = $dbdef_table->columns;
+
+if ($cgi->param('error')) { %>
+   <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+   <BR><BR>
+<% } 
+%>
+<A HREF="<%=$p2%>edit/<%=$table%>.cgi"><I>Add a new <%=$table%></I></A><BR><BR>
+
+<%=table()%>
+<TH>
+<% foreach (grep { $_ ne $pkey } @col) {
+  %><TD><%=$_%></TD>
+  <% } %>
+</TH>
+<% foreach $rec (sort {$a->getfield($pkey) cmp $b->getfield($pkey) } @rec) { 
+  %>
+  <TR>
+    <TD>
+      <A HREF="<%=$p2%>edit/<%=$table%>.cgi?<%=$rec->getfield($pkey)%>">
+      <%=$rec->getfield($pkey)%></A> </TD> <%
+  foreach $col (grep { $_ ne $pkey } @col)  { %>
+    <TD><%=$rec->getfield($col)%></TD> <% } %>
+  </A>
+  </TR>
+<% } %>
+</TABLE>
+</BODY>
+</HTML>
+
diff --git a/httemplate/browse/part_sb_field.cgi b/httemplate/browse/part_sb_field.cgi
new file mode 100644 (file)
index 0000000..4c9641e
--- /dev/null
@@ -0,0 +1,31 @@
+<%= header('svc_broadband extended fields', menubar('Main Menu'   => $p)) %>
+<%
+
+my @psf = qsearch('part_sb_field', {});
+my $block;
+my $p2 = popurl(2);
+
+%>
+
+<% if ($cgi->param('error')) { %>
+   <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+   <BR><BR>
+<% } %>
+
+<A HREF="<%=$p2%>edit/part_sb_field.cgi"><I>Add a new field</I></A><BR><BR>
+
+<%=table()%>
+<TH><TD>Field name</TD><TD>Service type</TD></TH>
+<% foreach $psf (sort {$a->name cmp $b->name} @psf) { %>
+  <TR>
+    <TD></TD>
+    <TD>
+      <A HREF="<%=$p2%>edit/part_sb_field.cgi?<%=$psf->sbfieldpart%>">
+        <%=$psf->name%></A></TD>
+    <TD><%=$psf->part_svc->svc%></TD>
+  </TR>
+<% } %>
+</TABLE>
+</BODY>
+</HTML>
+
diff --git a/httemplate/browse/router.cgi b/httemplate/browse/router.cgi
new file mode 100644 (file)
index 0000000..8864936
--- /dev/null
@@ -0,0 +1,37 @@
+<%= header('Routers', menubar('Main Menu'   => $p)) %>
+<%
+
+my @router = qsearch('router', {});
+my $p2 = popurl(2);
+
+%>
+
+<% if ($cgi->param('error')) { %>
+   <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+   <BR><BR>
+<% } %>
+
+<A HREF="<%=$p2%>edit/router.cgi"><I>Add a new router</I></A><BR><BR>
+
+<%=table()%>
+<!-- <TH><TD>Field name</TD><TD>Field value</TD></TH> -->
+<% foreach $router (sort {$a->routernum <=> $b->routernum} @router) { %>
+  <TR>
+<!--    <TD ROWSPAN="<%=scalar($router->router_field) + 2%>"> -->
+    <TD>
+      <A HREF="<%=$p2%>edit/router.cgi?<%=$router->routernum%>"><%=$router->routername%></A>
+    </TD>
+  <!-- 
+  <% foreach (sort { $a->part_router_field->name cmp $b->part_router_field->name } $router->router_field )  { %>
+  <TR>
+    <TD BGCOLOR="#cccccc" ALIGN="right"><%=$_->part_router_field->name%></TD>
+    <TD BGCOLOR="#ffffff"><%=$_->value%></TD>
+  </TR>
+  <% } %>
+  -->
+  </TR>
+<% } %>
+</TABLE>
+</BODY>
+</HTML>
+
diff --git a/httemplate/edit/part_router_field.cgi b/httemplate/edit/part_router_field.cgi
new file mode 100644 (file)
index 0000000..c3e99be
--- /dev/null
@@ -0,0 +1,70 @@
+<!-- mason kludge -->
+<%
+my ($routerfieldpart, $part_router_field);
+
+if ( $cgi->param('error') ) {
+  $part_router_field = new FS::part_router_field ( {
+    map { $_, scalar($cgi->param($_)) } fields('part_router_field')});
+  $routerfieldpart = $part_router_field->routerfieldpart;
+} else {
+  my($query) = $cgi->keywords;
+  if ( $query =~ /^(\d+)$/ ) { #editing
+    $routerfieldpart=$1;
+    $part_router_field=qsearchs('part_router_field',
+        {'routerfieldpart' => $routerfieldpart})
+      or die "Unknown routerfieldpart!";
+  
+  } else { #adding
+    $part_router_field = new FS::part_router_field({});
+  }
+}
+my $action = $part_router_field->routerfieldpart ? 'Edit' : 'Add';
+
+my $p1 = popurl(1);
+print header("$action Router Extended Field Definition", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+%>
+<FORM ACTION="<%=$p1%>process/generic.cgi" METHOD=POST>
+
+<INPUT TYPE="hidden" NAME="table" VALUE="part_router_field">
+<INPUT TYPE="hidden" NAME="redirect_ok" 
+    VALUE="<%=$p1%>part_router_field.cgi">
+<INPUT TYPE="hidden" NAME="routerfieldpart" VALUE="<%=
+  $routerfieldpart%>">
+Field #<B><%=$routerfieldpart or "(NEW)"%></B><BR><BR>
+
+<%=ntable("#cccccc",2)%>
+  <TR>
+    <TD ALIGN="right">Name</TD>
+    <TD><INPUT TYPE="text" NAME="name" MAXLENGTH=15 VALUE="<%=
+    $part_router_field->name%>"></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">Length</TD>
+    <TD><INPUT TYPE="text" NAME="length" MAXLENGTH=4 VALUE="<%=
+    $part_router_field->length%>"></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">check_block</TD>
+    <TD><TEXTAREA COLS="20" ROWS="4" NAME="check_block"><%=
+    $part_router_field->check_block%></TEXTAREA></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">list_source</TD>
+    <TD><TEXTAREA COLS="20" ROWS="4" NAME="list_source"><%=
+    $part_router_field->list_source%></TEXTAREA></TD>
+  </TR>
+</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">
+
+</FORM>
+
+<BR><BR>
+<FONT SIZE=-2>If you don't understand what <I>check_block</I> and 
+<I>list_source</I> mean, <B>LEAVE THEM BLANK</B>.  We mean it.</FONT>
+
+
+</BODY>
+</HTML>
diff --git a/httemplate/edit/part_sb_field.cgi b/httemplate/edit/part_sb_field.cgi
new file mode 100644 (file)
index 0000000..9e0cc9e
--- /dev/null
@@ -0,0 +1,79 @@
+<!-- mason kludge -->
+<%
+my ($sbfieldpart, $part_sb_field);
+
+if ( $cgi->param('error') ) {
+  $part_sb_field = new FS::part_sb_field ( {
+    map { $_, scalar($cgi->param($_)) } fields('part_sb_field')});
+  $sbfieldpart = $part_sb_field->sbfieldpart;
+} else {
+  my($query) = $cgi->keywords;
+  if ( $query =~ /^(\d+)$/ ) { #editing
+    $sbfieldpart=$1;
+    $part_sb_field=qsearchs('part_sb_field',
+        {'sbfieldpart' => $sbfieldpart})
+      or die "Unknown sbfieldpart!";
+  
+  } else { #adding
+    $part_sb_field = new FS::part_sb_field({});
+  }
+}
+my $action = $part_sb_field->sbfieldpart ? 'Edit' : 'Add';
+
+my $p1 = popurl(1);
+print header("$action svc_broadband Extended Field Definition", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+%>
+<FORM ACTION="<%=$p1%>process/generic.cgi" METHOD="POST">
+
+<INPUT TYPE="hidden" NAME="table" VALUE="part_sb_field">
+<INPUT TYPE="hidden" NAME="redirect_ok" 
+    VALUE="<%=popurl(2)%>browse/part_sb_field.cgi">
+<INPUT TYPE="hidden" NAME="sbfieldpart" VALUE="<%=
+  $sbfieldpart%>">
+Field #<B><%=$sbfieldpart or "(NEW)"%></B><BR><BR>
+
+<%=ntable("#cccccc",2)%>
+  <TR>
+    <TD ALIGN="right">Name</TD>
+    <TD><INPUT TYPE="text" NAME="name" MAXLENGTH=15 VALUE="<%=
+    $part_sb_field->name%>"></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">Length</TD>
+    <TD><INPUT TYPE="text" NAME="length" MAXLENGTH=4 VALUE="<%=
+    $part_sb_field->length%>"></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">Service</TD>
+    <TD><SELECT SIZE=1 NAME="svcpart"><%
+      foreach my $part_svc (qsearch('part_svc', {svcdb => 'svc_broadband'})) {
+        %><OPTION VALUE="<%=$part_svc->svcpart%>"<%=
+         ($part_svc->svcpart == $part_sb_field->svcpart) ? ' SELECTED' : ''%>">
+         <%=$part_svc->svc%>
+      <% } %>
+      </SELECT></TD>
+  <TR>
+    <TD ALIGN="right">check_block</TD>
+    <TD><TEXTAREA COLS="20" ROWS="4" NAME="check_block"><%=
+    $part_sb_field->check_block%></TEXTAREA></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">list_source</TD>
+    <TD><TEXTAREA COLS="20" ROWS="4" NAME="list_source"><%=
+    $part_sb_field->list_source%></TEXTAREA></TD>
+  </TR>
+</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">
+
+</FORM>
+
+<BR><BR>
+<FONT SIZE=-2>If you don't understand what <I>check_block</I> and 
+<I>list_source</I> mean, <B>LEAVE THEM BLANK</B>.  We mean it.</FONT>
+
+
+</BODY>
+</HTML>
diff --git a/httemplate/edit/process/addr_block/add.cgi b/httemplate/edit/process/addr_block/add.cgi
new file mode 100755 (executable)
index 0000000..34d799c
--- /dev/null
@@ -0,0 +1,20 @@
+<%
+
+my $error = '';
+my $ip_gateway = $cgi->param('ip_gateway');
+my $ip_netmask = $cgi->param('ip_netmask');
+
+my $new = new FS::addr_block {
+    ip_gateway => $ip_gateway,
+    ip_netmask => $ip_netmask,
+    routernum  => 0 };
+
+$error = $new->insert;
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi?". $cgi->query_string );
+} else { 
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi");
+} 
+%>
diff --git a/httemplate/edit/process/addr_block/allocate.cgi b/httemplate/edit/process/addr_block/allocate.cgi
new file mode 100755 (executable)
index 0000000..85b0d7a
--- /dev/null
@@ -0,0 +1,25 @@
+<%
+my $error = '';
+my $blocknum = $cgi->param('blocknum');
+my $routernum = $cgi->param('routernum');
+
+my $addr_block = qsearchs('addr_block', { blocknum => $blocknum });
+my $router = qsearchs('router', { routernum => $routernum });
+
+if($addr_block) {
+  if ($router) {
+    $error = $addr_block->allocate($router);
+  } else {
+    $error = "Cannot find router with routernum $routernum";
+  }
+} else {
+  $error = "Cannot find block with blocknum $blocknum";
+}
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi?" . $cgi->query_string);
+} else { 
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi");
+}
+%>
diff --git a/httemplate/edit/process/addr_block/deallocate.cgi b/httemplate/edit/process/addr_block/deallocate.cgi
new file mode 100755 (executable)
index 0000000..cfb7ed0
--- /dev/null
@@ -0,0 +1,24 @@
+<%
+my $error = '';
+my $blocknum = $cgi->param('blocknum');
+
+my $addr_block = qsearchs('addr_block', { blocknum => $blocknum });
+
+if($addr_block) {
+  my $router = $addr_block->router;
+  if ($router) {
+    $error = $addr_block->deallocate($router);
+  } else {
+    $error = "Block is not allocated to a router";
+  }
+} else {
+  $error = "Cannot find block with blocknum $blocknum";
+}
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi?" . $cgi->query_string);
+} else { 
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi");
+}
+%>
diff --git a/httemplate/edit/process/addr_block/split.cgi b/httemplate/edit/process/addr_block/split.cgi
new file mode 100755 (executable)
index 0000000..bb6d4ba
--- /dev/null
@@ -0,0 +1,19 @@
+<%
+my $error = '';
+my $blocknum = $cgi->param('blocknum');
+my $addr_block = qsearchs('addr_block', { blocknum => $blocknum });
+
+if ( $addr_block) {
+  $error = $addr_block->split_block;
+} else {
+  $error = "Unknown blocknum: $blocknum";
+}
+
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi?". $cgi->query_string );
+} else { 
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi");
+} 
+%>
diff --git a/httemplate/edit/process/generic.cgi b/httemplate/edit/process/generic.cgi
new file mode 100644 (file)
index 0000000..751987f
--- /dev/null
@@ -0,0 +1,69 @@
+<%
+
+# Welcome to generic.cgi.
+# 
+# This script provides a generic edit/process/ backend for simple table 
+# editing.  All it knows how to do is take the values entered into 
+# the script and insert them into the table specified by $cgi->param('table').
+# If there's an existing record with the same primary key, it will be 
+# replaced.  (Deletion will be added in the future.)
+# 
+# Special cgi params for this script:
+# table: the name of the table to be edited.  The script will die horribly 
+#        if it can't find the table.
+# redirect_ok: URL to be displayed after a successful edit.  The value of 
+#              the record's primary key will be passed as a keyword.
+#              Defaults to (freeside root)/view/$table.cgi.
+# redirect_error: URL to be displayed if there's an error.  The original 
+#                 query string, plus the error message, will be passed.
+#                 Defaults to $cgi->referer() (i.e. go back where you 
+#                 came from).
+
+
+use FS::Record qw(qsearchs dbdef);
+use DBIx::DBSchema;
+use DBIx::DBSchema::Table;
+
+
+my $error;
+my $p2 = popurl(2);
+my $table = $cgi->param('table');
+my $dbdef = dbdef or die "Cannot fetch dbdef!";
+
+my $dbdef_table = $dbdef->table($table) or die "Cannot fetch schema for $table";
+
+my $pkey = $dbdef_table->primary_key or die "Cannot fetch pkey for $table";
+my $pkey_val = $cgi->param($pkey);
+
+
+#warn "new FS::Record ( $table, (hashref) )";
+my $new = FS::Record::new ( "FS::$table", {
+    map { $_, scalar($cgi->param($_)) } fields($table) 
+} );
+
+#warn 'created $new of class '.ref($new);
+
+if($pkey_val and (my $old = qsearchs($table, { $pkey, $pkey_val} ))) {
+  # edit
+  $error = $new->replace($old);
+} else {
+  #add
+  $error = $new->insert;
+  $pkey_val = $new->getfield($pkey);
+  # New records usually don't have their primary keys set until after 
+  # they've been checked/inserted, so grab the new $pkey_val so we can 
+  # redirect to it.
+}
+
+my $redirect_ok = (($cgi->param('redirect_ok')) ?
+                    $cgi->param('redirect_ok') : $p2."view/$table.cgi");
+my $redirect_error = (($cgi->param('redirect_error')) ?
+                       $cgi->param('redirect_error') : $cgi->referer());
+
+if($error) {
+  $cgi->param('error', $error);
+  print $cgi->redirect($redirect_error . '?' . $cgi->query_string);
+} else {
+  print $cgi->redirect($redirect_ok . '?' .$pkey_val);
+}
+%>
diff --git a/httemplate/edit/process/router.cgi b/httemplate/edit/process/router.cgi
new file mode 100644 (file)
index 0000000..c0cb884
--- /dev/null
@@ -0,0 +1,100 @@
+<%
+
+use FS::UID qw(dbh);
+
+my $dbh = dbh;
+local $FS::UID::AutoCommit=0;
+
+sub check {
+  my $error = shift;
+  if($error) {
+    $cgi->param('error', $error);
+    print $cgi->redirect(popurl(3) . "edit/router.cgi?". $cgi->query_string);
+    $dbh->rollback;
+    exit;
+  }
+}
+
+my $error = '';
+my $routernum  = $cgi->param('routernum');
+my $routername = $cgi->param('routername');
+my $old = qsearchs('router', { routernum => $routernum });
+my @old_rf;
+my @old_psr;
+
+my $new = new FS::router {
+    routernum  => $routernum,
+    routername => $routername,
+    svcnum     => 0
+    };
+
+if($old) {
+  if($old->routername ne $new->routername) {
+    $error = $new->replace($old);
+  } #else do nothing
+} else {
+  $error = $new->insert;
+}
+
+check($error);
+
+if ($old) {
+  @old_psr = $old->part_svc_router;
+  foreach $psr (@old_psr) {
+    if($cgi->param('svcpart_'.$psr->svcpart) eq 'ON') {
+      # do nothing
+    } else {
+      $error = $psr->delete;
+    }
+  }
+  check($error);
+  @old_rf = $old->router_field;
+  foreach $rf (@old_rf) {
+    if(my $new_val = $cgi->param('rf_'.$rf->routerfieldpart)) {
+      if($new_val ne $rf->value) {
+        my $new_rf = new FS::router_field 
+         { routernum       => $routernum,
+           value           => $new_val,
+           routerfieldpart => $rf->routerfieldpart };
+       $error = $new_rf->replace($rf);
+      } #else do nothing
+    } else {
+      $error = $rf->delete;
+    }
+    check($error);
+  }
+}
+
+foreach($cgi->param) {
+  if($cgi->param($_) eq 'ON' and /^svcpart_(\d+)$/) {
+    my $svcpart = $1;
+    if(grep {$_->svcpart == $svcpart} @old_psr) {
+      # do nothing
+    } else {
+      my $new_psr = new FS::part_svc_router { svcpart   => $svcpart,
+                                              routernum => $routernum };
+      $error = $new_psr->insert;
+    }
+    check($error);
+  } elsif($cgi->param($_) ne '' and /^rf_(\d+)$/) {
+    my $part = $1;
+    if(my @x = grep {$_->routerfieldpart == $part} @old_rf) {
+      # already handled all of these
+    } else {
+      my $new_rf = new FS::router_field
+        { routernum       => $routernum,
+         value           => $cgi->param('rf_'.$part),
+         routerfieldpart => $part };
+      $error = $new_rf->insert;
+      check($error);
+    }
+  }
+}
+
+
+
+# Yay, everything worked!
+$dbh->commit or die $dbh->errstr;
+print $cgi->redirect(popurl(3). "edit/router.cgi?$routernum");
+
+%>
index fd7ba20..ab8b9f9 100644 (file)
@@ -1,12 +1,19 @@
 <%
 
+# If it's stupid but it works, it's not stupid.
+# -- U.S. Army
+
+local $FS::UID::AutoCommit = 0;
+my $dbh = FS::UID::dbh;
+
 $cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
 my $svcnum = $1;
 
-my $old;
+my $old; my @old_sbf;
 if ( $svcnum ) {
   $old = qsearchs('svc_broadband', { 'svcnum' => $svcnum } )
     or die "fatal: can't find broadband service (svcnum $svcnum)!";
+  @old_sbf = $old->sb_field;
 } else {
   $old = '';
 }
@@ -17,14 +24,6 @@ my $new = new FS::svc_broadband ( {
   } ( 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);
@@ -33,12 +32,47 @@ if ( $svcnum ) {
   $svcnum = $new->svcnum;
 }
 
+unless ($error) {
+  my $sb_field;
+
+  foreach ($cgi->param) {
+    #warn "\$cgi->param $_: " . $cgi->param($_);
+    if(/^sbf_(\d+)/) {
+      my $part = $1;
+      #warn "\$part $part";
+      $sb_field = new FS::sb_field 
+        { svcnum      => $svcnum,
+          value       => $cgi->param($_),
+          sbfieldpart => $part };
+      if (my @x = grep { $_->sbfieldpart eq $part } @old_sbf) {
+      #if (my $old_sb_field = (grep { $_->sbfieldpart eq $part} @old_Sbf)[0]) {
+        #warn "array: " . scalar(@x);
+        if (length($sb_field->value) && ($sb_field->value ne $x[0]->value)) { 
+          #warn "replacing " . $x[0]->value . " with " . $sb_field->value;
+          $error = $sb_field->replace($x[0]);
+          #$error = $sb_field->replace($old_sb_field);
+        } elsif (length($sb_field->value) == 0) { 
+          #warn "delete";
+          $error = $x[0]->delete;
+        }
+      } else {
+        if (length($sb_field->value) > 0) { 
+          #warn "insert";
+          $error = $sb_field->insert;
+        }
+        # else do nothing
+      }
+    }
+  }
+}
+
 if ( $error ) {
   $cgi->param('error', $error);
   $cgi->param('ip_addr', $new->ip_addr);
-  $cgi->param('mac_addr', $new->mac_addr);
+  $dbh->rollback;
   print $cgi->redirect(popurl(2). "svc_broadband.cgi?". $cgi->query_string );
 } else {
+  $dbh->commit or die $dbh->errstr;
   print $cgi->redirect(popurl(3). "view/svc_broadband.cgi?" . $svcnum );
 }
 
diff --git a/httemplate/edit/router.cgi b/httemplate/edit/router.cgi
new file mode 100755 (executable)
index 0000000..d2279ff
--- /dev/null
@@ -0,0 +1,88 @@
+<HTML><BODY>
+
+<%
+
+my $router;
+if ( $cgi->keywords ) {
+  my($query) = $cgi->keywords;
+  $query =~ /^(\d+)$/;
+  $router = qsearchs('router', { routernum => $1 }) 
+      or print $cgi->redirect(popurl(2)."browse/router.cgi") ;
+} else {
+  $router = new FS::router ( {
+    map { $_, scalar($cgi->param($_)) } fields('router')
+  } );
+}
+
+my $routernum = $router->routernum;
+my $action = $routernum ? 'Edit' : 'Add';
+my $hashref = $router->hashref;
+
+print header("$action Router", menubar(
+  'Main Menu' => "$p",
+  'View all routers' => "${p}browse/router.cgi",
+));
+
+if($cgi->param('error')) {
+%> <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+<% } %>
+
+<FORM ACTION="<%=popurl(1)%>process/router.cgi" METHOD=POST>
+  <INPUT TYPE="hidden" NAME="routernum" VALUE="<%=$routernum%>">
+    Router #<%=$routernum or "(NEW)"%>
+
+<BR><BR>Name <INPUT TYPE="text" NAME="routername" SIZE=32 VALUE="<%=$hashref->{routername}%>">
+<%=table() %>
+
+<%
+# I know, I know.  Massive false laziness with edit/svc_broadband.cgi.  But 
+# Kristian won't let me generalize the custom field mechanism to every table in 
+# the database, so this is what we get.  <snarl>
+# -- MW
+
+my @part_router_field = qsearch('part_router_field', { });
+my %rf = map { $_->part_router_field->name, $_->value } $router->router_field;
+foreach (sort { $a->name cmp $b->name } @part_router_field) {
+  %>
+  <TR>
+    <TD ALIGN="right"><%=$_->name%></TD>
+    <TD><%
+  if(my @opts = $_->list_values) {
+    %>  <SELECT NAME="rf_<%=$_->routerfieldpart%>" SIZE="1">
+          <%
+    foreach $opt (@opts) {
+      %>  <OPTION VALUE="<%=$opt%>"<%=($opt eq $rf{$_->name}) 
+              ? ' SELECTED' : ''%>>
+            <%=$opt%>
+         </OPTION>
+   <% } %>
+       </SELECT>
+ <% } else { %>
+        <INPUT NAME="rf_<%=$_->routerfieldpart%>"
+        VALUE="<%=$rf{$_->name}%>"
+        <%=$_->length ? 'SIZE="'.$_->length.'"' : ''%>>
+  <% } %></TD>
+  </TR>
+<% } %>
+</TABLE>
+
+
+
+<BR><BR>Select the service types available on this router<BR>
+<%
+
+foreach my $part_svc ( qsearch('part_svc', { svcdb    => 'svc_broadband',
+                                             disabled => '' }) ) {
+  %>
+  <BR>
+  <INPUT TYPE="checkbox" NAME="svcpart_<%=$part_svc->svcpart%>"<%=
+      qsearchs('part_svc_router', { svcpart   => $part_svc->svcpart, 
+                                    routernum => $routernum } ) ? 'CHECKED' : ''%> VALUE="ON">
+  <A HREF="<%=${p}%>edit/part_svc.cgi?<%=$part_svc->svcpart%>">
+    <%=$part_svc->svcpart%>: <%=$part_svc->svc%></A>
+  <% } %>
+
+  <BR><BR><INPUT TYPE="submit" VALUE="Apply changes">
+  </FORM>
+</BODY></HTML>
+
index d8a1f7a..f017d7a 100644 (file)
@@ -1,6 +1,13 @@
 <!-- mason kludge -->
 <%
 
+# If it's stupid but it works, it's still stupid.
+#  -Kristian
+
+
+use HTML::Widgets::SelectLayers;
+use Tie::IxHash;
+
 my( $svcnum,  $pkgnum, $svcpart, $part_svc, $svc_broadband );
 if ( $cgi->param('error') ) {
   $svc_broadband = new FS::svc_broadband ( {
@@ -38,6 +45,8 @@ if ( $cgi->param('error') ) {
     $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
     die "No part_svc entry!" unless $part_svc;
 
+    $svc_broadband->setfield('svcpart', $svcpart);
+
     $svcnum='';
 
     #set fixed and default fields from part_svc
@@ -53,12 +62,9 @@ if ( $cgi->param('error') ) {
 }
 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') });
+  #Nothing?
 
 } elsif ( $action eq 'Edit' ) {
 
@@ -68,152 +74,111 @@ if ($pkgnum) {
   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>!;
-}
+my ($ip_addr, $speed_up, $speed_down, $blocknum) =
+    ($svc_broadband->ip_addr,
+     $svc_broadband->speed_up,
+     $svc_broadband->speed_down,
+     $svc_broadband->blocknum);
 
-#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
+%>
 
+<%=header("Broadband Service $action", '')%>
+
+<% if ($cgi->param('error')) { %>
+<FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT><BR>
+<% } %>
+
+Service #<B><%=$svcnum ? $svcnum : "(NEW)"%></B><BR><BR>
+
+<FORM ACTION="<%=${p1}%>process/svc_broadband.cgi" METHOD=POST>
+  <INPUT TYPE="hidden" NAME="svcnum" VALUE="<%=$svcnum%>">
+  <INPUT TYPE="hidden" NAME="pkgnum" VALUE="<%=$pkgnum%>">
+  <INPUT TYPE="hidden" NAME="svcpart" VALUE="<%=$svcpart%>">
+  <INPUT TYPE="hidden" NAME="ip_addr" VALUE="<%=$ip_addr%>">
+
+  <%=&ntable("#cccccc",2)%>
+
+    <TR>
+      <TD ALIGN="right">Download speed</TD>
+      <TD BGCOLOR="#ffffff">
+<% if ( $part_svc->part_svc_column('speed_down')->columnflag eq 'F' ) { %>
+        <INPUT TYPE="hidden" NAME="speed_down" VALUE="<%=$speed_down%>"><%=$speed_down%>Kbps
+<% } else { %>
+    <INPUT TYPE="text" NAME="speed_down" SIZE=5 VALUE="<%=$speed_down%>">Kbps
+<% } %>
+      </TD>
+    </TR>
+    <TR>
+      <TD ALIGN="right">Upload speed</TD>
+      <TD BGCOLOR="#ffffff">
+<% if ( $part_svc->part_svc_column('speed_up')->columnflag eq 'F' ) { %>
+        <INPUT TYPE="hidden" NAME="speed_up" VALUE="<%=$speed_up%>"><%=$speed_up%>Kbps
+<% } else { %>
+        <INPUT TYPE="text" NAME="speed_up" SIZE=5 VALUE="<%=$speed_up%>">Kbps
+<% } %>
+      </TD>
+    </TR>
+<% if ($action eq 'Add') { %>
+    <TR>
+      <TD ALIGN="right">Router/Block</TD>
+      <TD BGCOLOR="#ffffff">
+        <SELECT NAME="blocknum">
+<%
+  foreach my $router ($svc_broadband->allowed_routers) {
+    foreach my $addr_block ($router->addr_block) {
+%>
+        <OPTION VALUE="<%=$addr_block->blocknum%>"<%=($addr_block->blocknum eq $blocknum) ? ' SELECTED' : ''%>>
+          <%=$router->routername%>:<%=$addr_block->ip_gateway%>/<%=$addr_block->ip_netmask%></OPTION>
+<%
+    }
   }
-}
-
-#location
-my $location = $svc_broadband->location;
+%>
+        </SELECT>
+      </TD>
+    </TR>
+<% } else { %>
 
-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>!;
-}
+    <TR>
+      <TD ALIGN="right">Router/Block</TD>
+      <TD BGCOLOR="#ffffff">
+        <%=$svc_broadband->addr_block->router->routername%>:<%=$svc_broadband->addr_block->NetAddr%>
+        <INPUT TYPE="hidden" NAME="blocknum" VALUE="<%=$svc_broadband->blocknum%>">
+      </TD>
+    </TR>
 
-print '</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">';
+<% } %>
 
-print <<END;
+<%
 
-    </FORM>
-  </BODY>
+  my @part_sb_field = qsearch('part_sb_field', { svcpart => $svcpart });
+  my $sbf_hashref = $svc_broadband->sb_field_hashref($svcpart);
+  foreach (sort { $a->name cmp $b->name } @part_sb_field) {
+    %>
+    <TR>
+      <TD ALIGN="right"><%=$_->name%></TD>
+      <TD><%
+      if(my @opts = $_->list_values) {
+        %>
+       <SELECT NAME="sbf_<%=$_->sbfieldpart%>" SIZE=1> <%
+        foreach $opt (@opts) { %>
+          <OPTION VALUE="<%=$opt%>"<%=
+           ($opt eq $sbf_hashref->{$_->name}) ? ' SELECTED' : ''%>>
+           <%=$opt%></OPTION><%
+        } %></SELECT>
+   <% } else { %>
+        <INPUT NAME="sbf_<%=$_->sbfieldpart%>"
+           VALUE="<%=$sbf_hashref->{$_->name}%>"
+     <%=$_->length ? 'SIZE="'.$_->length.'"' : ''%>>
+   <% } %>
+      </TD>
+    </TR>
+<% } %>
+  </TABLE>
+  <BR>
+  <INPUT TYPE="submit" NAME="submit" VALUE="Submit">
+</FORM>
+</BODY>
 </HTML>
-END
-%>
+
index e8c3681..d13649b 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.
+         <LI><A HREF="browse/msgcat.cgi">View/Edit message catalog</A> - Change error messages and other customizable labels.
+         <LI><A HREF="browse/part_sb_field.cgi">View/Edit custom svc_broadband fields</A>
+         - Custom broadband service fields for site-specific export/informational data.
+         <LI><A HREF="browse/generic.cgi?part_router_field">View/Edit custom router fields</A>
+         - Custom router fields for site-specific export data.
+         <LI><A HREF="browse/router.cgi">View/Edit routers</A>
+         - Broadband access routers
+         <LI><A HREF="browse/addr_block.cgi">View/Edit address blocks</A>
+         - Manage address blocks and block assignments to broadband routers.
         </ul>
         <BR>
       </TD></TR>
index 156edfa..164b5b2 100644 (file)
@@ -20,28 +20,26 @@ if ($pkgnum) {
 }
 #eofalse
 
-my $ac = qsearchs('ac', { acnum => $svc_broadband->getfield('acnum') });
+my $router = $svc_broadband->addr_block->router;
+
+if (not $router) { die "Could not lookup router for svc_broadband (svcnum $svcnum)" };
 
 my (
-     $acname,
-     $acnum,
+     $routername,
+     $routernum,
      $speed_down,
      $speed_up,
-     $ip_addr,
-     $ip_netmask,
-     $mac_addr,
-     $location
+     $ip_addr
    ) = (
-     $ac->getfield('acname'),
-     $ac->getfield('acnum'),
+     $router->getfield('routername'),
+     $router->getfield('routernum'),
      $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')
+     $svc_broadband->getfield('ip_addr')
    );
 
+
+
 print header('Broadband Service View', menubar(
   ( ( $custnum )
     ? ( "View this package (#$pkgnum)" => "${p}view/cust_pkg.cgi?$pkgnum",
@@ -56,20 +54,38 @@ print header('Broadband Service View', menubar(
       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">Router</TD>!.
+        qq!<TD BGCOLOR="#ffffff">$routernum: $routername</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).
+      qq!<TR><TD ALIGN="right">IP Address</TD>!.
+        qq!<TD BGCOLOR="#ffffff">$ip_addr</TD></TR>!.
+      '</TD></TR><TR ROWSPAN="1"><TD></TD></TR>';
+
+
+#  foreach my $sb_field 
+#      ( qsearch('sb_field', { svcnum => $svcnum }) ) {
+#    my $part_sb_field = qsearchs('part_sb_field',
+#                         { sbfieldpart => $sb_field->sbfieldpart });
+#    print q!<TR><TD ALIGN="right">! . $part_sb_field->name . 
+#          q!</TD><TD BGCOLOR="#ffffff">! . $sb_field->value . 
+#          q!</TD></TR>!;
+#  }
+#  print '</TABLE>';
+
+
+  my $sb_field = $svc_broadband->sb_field_hashref;
+  foreach (sort { $a cmp $b } keys(%{$sb_field})) {
+    print q!<TR><TD ALIGN="right">! . $_ . 
+          q!</TD><TD BGCOLOR="#ffffff">! . $sb_field->{$_} .
+          q!</TD></TR>!;
+  }
+  print '</TABLE>';
+
+
+print '<BR>'. joblisting({'svcnum'=>$svcnum}, 1).
       '</BODY></HTML>'
 ;
 %>