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 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::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 );
 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:
 
 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:
 
 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 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
 
 =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
 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.
 
 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
 =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
 =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.
 
 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.
 
 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
 
 defined.  An FS::cust_svc record will be created and inserted.
 
 =cut
 
-# sub insert {}
 # Standard FS::svc_Common::insert
 # Standard FS::svc_Common::insert
-# (any necessary Deep Magic is handled by exports)
 
 =item delete
 
 
 =item delete
 
@@ -159,19 +115,62 @@ returns the error, otherwise returns false.
 =cut
 
 # Standard FS::svc_Common::replace
 =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
 
 
 =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
 
 
 =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
 
 
 =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
 
 
 =item check
 
@@ -189,105 +188,99 @@ sub check {
 
   my $error =
     $self->ut_numbern('svcnum')
 
   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_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'; }
 
   ;
   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
 }
 
   ''; #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
 
 
 =cut
 
-sub ac_block {
+sub NetAddr {
   my $self = shift;
   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
 
 
 =cut
 
-sub ac_type {
+sub addr_block {
   my $self = shift;
   my $self = shift;
-  return qsearchs('ac_type', { actypenum => $self->actypenum });
+
+  return qsearchs('addr_block', { blocknum => $self->blocknum });
 }
 
 =back
 
 }
 
 =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
 
 =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
 
 =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
 
 
 =cut
 
index 0ef3fc8..1948376 100755 (executable)
@@ -1012,76 +1012,99 @@ sub tables_hash_hack {
       'index'       => [],
     },
 
       'index'       => [],
     },
 
-    'ac_type' => {
+    'router' => {
       'columns' => [
       'columns' => [
-        'actypenum', 'serial', '', '',
-        'actypename', 'varchar', '', $char_d,
+        'routernum', 'serial', '', '',
+        'routername', 'varchar', '', $char_d,
+        'svcnum', 'int', '0', '',
       ],
       ],
-      'primary_key' => 'actypenum',
+      'primary_key' => 'routernum',
       'unique'      => [],
       'index'       => [],
     },
 
       'unique'      => [],
       'index'       => [],
     },
 
-    'ac' => {
+    'part_svc_router' => {
       'columns' => [
       'columns' => [
-        'acnum', 'serial', '', '',
-        'actypenum', 'int', '', '',
-        'acname', 'varchar', '', $char_d,
-      ],
-      'primary_key' => 'acnum',
+        'svcpart', 'int', '', '',
+       'routernum', 'int', '', '',
+      ];
+      'primary_key' => '',
       'unique'      => [],
       'unique'      => [],
-      'index'       => [ [ 'actypenum' ] ],
+      'index'       => [],
     },
 
     },
 
-    'part_ac_field' => {
+    'part_router_field' => {
       'columns' => [
       'columns' => [
-        'acfieldpart', 'serial', '', '',
-        'actypenum', 'int', '', '',
+        'routerfieldpart', 'serial', '', '',
         'name', 'varchar', '', $char_d,
         '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'      => [],
       'unique'      => [],
-      'index'       => [ [ 'actypenum' ] ],
+      'index'       => [],
     },
 
     },
 
-    'ac_field' => {
+    'router_field' => {
       'columns' => [
       'columns' => [
-        'acfieldpart', 'int', '', '',
-        'acnum', 'int', '', '',
-        'value', 'text', '', '',
+        'routerfieldpart', 'int', '', '',
+        'routernum', 'int', '', '',
+        'value', 'varchar', '', 128,
       ],
       'primary_key' => '',
       ],
       'primary_key' => '',
-      'unique'      => [ [ 'acfieldpart', 'acnum' ] ],
-      'index'       => [ [ 'acnum' ] ],
+      'unique'      => [ [ 'routerfieldpart', 'routernum' ] ],
+      'index'       => [],
     },
 
     },
 
-    'ac_block' => {
+    'addr_block' => {
       'columns' => [
       'columns' => [
-        'acnum', 'int', '', '',
+        'blocknum', 'int', '', '',
+       'routernum', 'int', '', '',
         'ip_gateway', 'varchar', '', 15,
         'ip_netmask', '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' => '',
       'primary_key' => '',
-      'unique'      => [],
-      'index'       => [ [ 'acnum' ] ],
+      'unique'      => [ [ 'sbfieldpart', 'svcnum' ] ],
+      'index'       => [],
     },
 
     'svc_broadband' => {
       'columns' => [
         'svcnum', 'int', '', '',
     },
 
     'svc_broadband' => {
       'columns' => [
         'svcnum', 'int', '', '',
-        'actypenum', 'int', '', '',
+        'blocknum', 'int', '', '',
         'speed_up', 'int', '', '',
         'speed_down', 'int', '', '',
         'speed_up', 'int', '', '',
         'speed_down', 'int', '', '',
-        'acnum', 'int', '', '',
         'ip_addr', 'varchar', '', 15,
         'ip_addr', 'varchar', '', 15,
-        'ip_netmask', 'int', '', '',
-        'mac_addr', 'char', '', 17,
-        'location', 'varchar', '', $char_d,
       ],
       'primary_key' => 'svcnum',
       'unique'      => [],
       ],
       '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_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);
 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::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;
 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_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);
       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::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;
       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;
 
 $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)!";
 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 = '';
 }
 } else {
   $old = '';
 }
@@ -17,14 +24,6 @@ my $new = new FS::svc_broadband ( {
   } ( fields('svc_broadband'), qw( pkgnum svcpart ) )
 } );
 
   } ( 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);
 my $error;
 if ( $svcnum ) {
   $error = $new->replace($old);
@@ -33,12 +32,47 @@ if ( $svcnum ) {
   $svcnum = $new->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);
 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 {
   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 );
 }
 
   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 -->
 <%
 
 <!-- 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 ( {
 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;
 
     $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
     $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 $action = $svc_broadband->svcnum ? 'Edit' : 'Add';
 
-my @ac_list;
-
 if ($pkgnum) {
 
 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' ) {
 
 
 } elsif ( $action eq 'Edit' ) {
 
@@ -68,152 +74,111 @@ if ($pkgnum) {
   die "\$action eq Add, but \$pkgnum is null!\n";
 }
 
   die "\$action eq Add, but \$pkgnum is null!\n";
 }
 
-
 my $p1 = popurl(1);
 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>
 </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 
               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/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>
         </ul>
         <BR>
       </TD></TR>
index 156edfa..164b5b2 100644 (file)
@@ -20,28 +20,26 @@ if ($pkgnum) {
 }
 #eofalse
 
 }
 #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 (
 
 my (
-     $acname,
-     $acnum,
+     $routername,
+     $routernum,
      $speed_down,
      $speed_up,
      $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('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",
 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>!.
       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">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>'
 ;
 %>
       '</BODY></HTML>'
 ;
 %>