[freeside-commits] branch master updated. 1af8ff7f48f7259fc99f090c301c84b9680fdb4d

Mark Wells mark at 420.am
Thu Nov 27 16:13:18 PST 2014


The branch, master has been updated
       via  1af8ff7f48f7259fc99f090c301c84b9680fdb4d (commit)
      from  0f0bc1ef7aafc6b3869c0f71ee2528c1c9897ce6 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 1af8ff7f48f7259fc99f090c301c84b9680fdb4d
Author: Mark Wells <mark at freeside.biz>
Date:   Thu Nov 27 15:21:29 2014 -0800

    svc_circuit, #23879, #25933, #30830

diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm
index bad831a..92cede6 100644
--- a/FS/FS/AccessRight.pm
+++ b/FS/FS/AccessRight.pm
@@ -310,6 +310,7 @@ tie my %rights, 'Tie::IxHash',
     'Services: Mailing lists',
     'Services: Alarm services',
     'Services: Video',
+    'Services: Circuits',
     'Services: External services',
     'Usage: RADIUS sessions',
     'Usage: Call Detail Records (CDRs)',
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 900da10..d3e45df 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -392,6 +392,10 @@ if ( -e $addl_handler_use_file ) {
   use FS::deploy_zone_vertex;
   use FS::TaxEngine;
   use FS::tax_status;
+  use FS::circuit_type;
+  use FS::circuit_provider;
+  use FS::circuit_termination;
+  use FS::svc_circuit;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 715a603..91dfc5d 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -5653,6 +5653,7 @@ sub tables_hashref {
         'max_simultaneous',               'int', 'NULL',      '', '', '',
         'e911_class',                    'char', 'NULL',       1, '', '',
         'e911_type',                     'char', 'NULL',       1, '', '', 
+        'circuit_svcnum',                 'int', 'NULL',      '', '', '',
       ],
       'primary_key'  => 'svcnum',
       'unique'       => [ [ 'sms_carrierid', 'sms_account'] ],
@@ -5678,6 +5679,10 @@ sub tables_hashref {
                             table      => 'cdr_carrier',
                             references => [ 'carrierid' ],
                           },
+                          { columns    => [ 'circuit_svcnum' ],
+                            table      => 'svc_circuit',
+                            references => [ 'svcnum' ],
+                          },
                         ],
     },
 
@@ -6527,6 +6532,75 @@ sub tables_hashref {
                         ],
     },
 
+    'circuit_type' => {
+      'columns' => [
+        'typenum',     'serial',     '',      '', '', '',
+        'typename',   'varchar',     '', $char_d, '', '',
+        'disabled',      'char', 'NULL',       1, '', '',
+        # speed? number of voice lines? anything else?
+      ],
+      'primary_key' => 'typenum',
+      'unique' => [ [ 'typename' ] ],
+      'index'  => [],
+    },
+
+    'circuit_provider' => {
+      'columns' => [
+        'providernum', 'serial',     '',      '', '', '',
+        'provider',   'varchar',     '', $char_d, '', '',
+        'disabled',      'char', 'NULL',       1, '', '', 
+      ],
+      'primary_key' => 'providernum',
+      'unique' => [ [ 'provider' ], ],
+      'index'  => [],
+    },
+
+    'circuit_termination' => {
+      'columns' => [
+        'termnum',     'serial',     '',      '', '', '',
+        'termination','varchar',     '', $char_d, '', '',
+        'disabled',      'char', 'NULL',       1, '', '',
+      ],
+      'primary_key' => 'termnum',
+      'unique' => [ [ 'termination' ] ],
+      'index' => [],
+    },
+
+    'svc_circuit' => {
+      'columns' => [
+        'svcnum',                   'int',     '', '', '', '',
+        'typenum',                  'int',     '', '', '', '',
+        'providernum',              'int',     '', '', '', '',
+        'termnum',                  'int',     '', '', '', '',
+        'circuit_id',           'varchar',     '', 64, '', '',
+        'desired_due_date',         'int', 'NULL', '', '', '',
+        'due_date',                 'int', 'NULL', '', '', '',
+        'vendor_order_id',      'varchar', 'NULL', $char_d,  '', '',
+        'vendor_qual_id',       'varchar', 'NULL', $char_d,  '', '',
+        'vendor_order_type',    'varchar', 'NULL', $char_d,  '', '',
+        'vendor_order_status',  'varchar', 'NULL', $char_d,  '', '',
+        'endpoint_ip_addr',     'varchar', 'NULL', 40, '', '',
+        'endpoint_mac_addr',    'varchar', 'NULL', 12, '', '',
+      ],
+      'primary_key' => 'svcnum',
+      'unique'      => [],
+      'index'       => [ [ 'providernum' ], [ 'typenum' ] ],
+      'foreign_keys' => [
+                          { columns => [ 'svcnum' ],
+                            table   => 'cust_svc',
+                          },
+                          { columns => [ 'typenum' ],
+                            table   => 'circuit_type',
+                          },
+                          { columns => [ 'providernum' ],
+                            table   => 'circuit_provider',
+                          },
+                          { columns => [ 'termnum' ],
+                            table   => 'circuit_termination',
+                          },
+      ],
+    },
+
     'vend_main' => {
       'columns' => [
         'vendnum',   'serial',     '',      '', '', '',
diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm
index 99c3560..e138692 100644
--- a/FS/FS/UI/Web.pm
+++ b/FS/FS/UI/Web.pm
@@ -113,16 +113,16 @@ sub svc_url {
     if $DEBUG;
   if ( $opt{m}->interp->comp_exists("/$opt{action}/$svcdb.cgi") ) {
     $url = "$svcdb.cgi?";
+  } elsif ( $opt{m}->interp->comp_exists("/$opt{action}/$svcdb.html") ) {
+    $url = "$svcdb.html?";
   } else {
-
     my $generic = $opt{action} eq 'search' ? 'cust_svc' : 'svc_Common';
 
     $url = "$generic.html?svcdb=$svcdb;";
     $url .= 'svcnum=' if $query =~ /^\d+(;|$)/ or $query eq '';
   }
 
-  import FS::CGI 'rooturl'; #WTF!  why is this necessary
-  my $return = rooturl(). "$opt{action}/$url$query";
+  my $return = FS::CGI::rooturl(). "$opt{action}/$url$query";
 
   $return = qq!<A HREF="$return">! if $opt{ahref};
 
@@ -574,6 +574,19 @@ sub cust_aligns {
   }
 }
 
+=item cust_links
+
+Returns an array of links to view/cust_main.cgi, for use with cust_fields.
+
+=cut
+
+sub cust_links {
+  my $link = [ FS::CGI::rooturl().'view/cust_main.cgi?', 'custnum' ];
+
+  return map { $_ eq 'cust_status_label' ? '' : $link }
+    @cust_fields;
+}
+
 =item is_mobile
 
 Utility function to determine if the client is a mobile browser.
diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm
index 3e62a68..ba0f61d 100755
--- a/FS/FS/addr_block.pm
+++ b/FS/FS/addr_block.pm
@@ -388,6 +388,24 @@ sub label {
   ($router ? $router->routername : '(unallocated)'). ':'. $self->NetAddr;
 }
 
+=item router
+
+Returns the router assigned to this block.
+
+=cut
+
+# necessary, because this can't be foreign keyed
+
+sub router {
+  my $self = shift;
+  my $routernum = $self->routernum;
+  if ( $routernum ) {
+    return FS::router->by_key($routernum);
+  } else {
+    return;
+  }
+}
+
 =back
 
 =head1 BUGS
diff --git a/FS/FS/circuit_provider.pm b/FS/FS/circuit_provider.pm
new file mode 100644
index 0000000..6cb7841
--- /dev/null
+++ b/FS/FS/circuit_provider.pm
@@ -0,0 +1,101 @@
+package FS::circuit_provider;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::circuit_provider - Object methods for circuit_provider records
+
+=head1 SYNOPSIS
+
+  use FS::circuit_provider;
+
+  $record = new FS::circuit_provider \%hash;
+  $record = new FS::circuit_provider { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::circuit_provider object represents a telecom carrier that provides
+physical circuits (L<FS::svc_circuit>).  FS::circuit_provider inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item providernum - primary key
+
+=item provider - provider name
+
+=item disabled - disabled
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+=cut
+
+sub table { 'circuit_provider'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('providernum')
+    || $self->ut_text('provider')
+    || $self->ut_flag('disabled')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/circuit_termination.pm b/FS/FS/circuit_termination.pm
new file mode 100644
index 0000000..3f0afc1
--- /dev/null
+++ b/FS/FS/circuit_termination.pm
@@ -0,0 +1,98 @@
+package FS::circuit_termination;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::circuit_termination - Object methods for circuit_termination records
+
+=head1 SYNOPSIS
+
+  use FS::circuit_termination;
+
+  $record = new FS::circuit_termination \%hash;
+  $record = new FS::circuit_termination { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::circuit_termination object represents a central office circuit 
+interface type.  FS::circuit_termination inherits from FS::Record.  The 
+following fields are currently supported:
+
+=over 4
+
+=item termnum - primary key
+
+=item termination - description of the termination type
+
+=item disabled - 'Y' if this is disabled
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new example.  To add the example to the database, see L<"insert">.
+
+=cut
+
+sub table { 'circuit_termination'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid example.  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('termnum')
+    || $self->ut_text('termination')
+    || $self->ut_flag('disabled')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/circuit_type.pm b/FS/FS/circuit_type.pm
new file mode 100644
index 0000000..3b36536
--- /dev/null
+++ b/FS/FS/circuit_type.pm
@@ -0,0 +1,98 @@
+package FS::circuit_type;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::circuit_type - Object methods for circuit_type records
+
+=head1 SYNOPSIS
+
+  use FS::circuit_type;
+
+  $record = new FS::circuit_type \%hash;
+  $record = new FS::circuit_type { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::circuit_type object represents a circuit type (such as "DS1" or "OC3").
+FS::circuit_type inherits from FS::Record.  The following fields are currently
+supported:
+
+=over 4
+
+=item typenum - primary key
+
+=item typename - name of the circuit type
+
+=item disabled - 'Y' if this is disabled
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new example.  To add the example to the database, see L<"insert">.
+
+=cut
+
+sub table { 'circuit_type'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid example.  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('typenum')
+    || $self->ut_text('typename')
+    || $self->ut_flag('disabled')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/router.pm b/FS/FS/router.pm
index 4011bb0..c0c93dd 100755
--- a/FS/FS/router.pm
+++ b/FS/FS/router.pm
@@ -200,6 +200,13 @@ sub delete {
 Returns a list of FS::addr_block objects (address blocks) associated
 with this object.
 
+=cut
+
+sub addr_block {
+  my $self = shift;
+  qsearch('addr_block', { routernum => $self->routernum });
+}
+
 =item auto_addr_block
 
 Returns a list of address blocks on which auto-assignment of IP addresses
diff --git a/FS/FS/svc_circuit.pm b/FS/FS/svc_circuit.pm
new file mode 100644
index 0000000..f705c68
--- /dev/null
+++ b/FS/FS/svc_circuit.pm
@@ -0,0 +1,230 @@
+package FS::svc_circuit;
+
+use strict;
+use base qw(
+  FS::svc_IP_Mixin
+  FS::MAC_Mixin
+  FS::svc_Common
+);
+use FS::Record qw( qsearch qsearchs );
+use FS::circuit_provider;
+use FS::circuit_type;
+use FS::circuit_termination;
+
+=head1 NAME
+
+FS::svc_circuit - Object methods for svc_circuit records
+
+=head1 SYNOPSIS
+
+  use FS::svc_circuit;
+
+  $record = new FS::svc_circuit \%hash;
+  $record = new FS::svc_circuit { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::svc_circuit object represents a telecom circuit service (other than 
+an analog phone line, which is svc_phone, or a DSL Internet connection, 
+which is svc_dsl).  FS::svc_circuit inherits from FS::svc_IP_Mixin,
+FS::MAC_Mixin, and FS::svc_Common.  The following fields are currently
+supported:
+
+=over 4
+
+=item svcnum - primary key; see also L<FS::cust_svc>
+
+=item typenum - circuit type (such as DS1, DS1-PRI, DS3, OC3, etc.); foreign
+key to L<FS::circuit_type>.
+
+=item providernum - circuit provider (telco); foreign key to 
+L<FS::circuit_provider>.
+
+=item termnum - circuit termination type; foreign key to 
+L<FS::circuit_termination>
+
+=item circuit_id - circuit ID string defined by the provider
+
+=item desired_due_date - the requested date for completion of the circuit
+order
+
+=item due_date - the provider's committed date for completion of the circuit
+order
+
+=item vendor_order_id - the provider's order number
+
+=item vendor_qual_id - the qualification number, if a qualification was 
+performed
+
+=item vendor_order_type -
+
+=item vendor_order_status - the order status: ACCEPTED, PENDING, COMPLETED,
+etc.
+
+=item endpoint_ip_addr - the IP address of the endpoint equipment, if any. 
+This will be validated as an IP address but not assigned from managed address
+space or checked for uniqueness.
+
+=item endpoint_mac_addr - the MAC address of the endpoint.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new circuit service.  To add the record to the database, see 
+L<"insert">.
+
+=cut
+
+sub table { 'svc_circuit'; }
+
+sub table_info {
+  my %dis = ( disable_default => 1, disable_fixed => 1,
+              disabled_inventory => 1, disable_select => 1 );
+
+  tie my %fields, 'Tie::IxHash', (
+    'svcnum'            => 'Service',
+    'providernum'       => {
+                              label         => 'Provider',
+                              type          => 'select',
+                              select_table  => 'circuit_provider',
+                              select_key    => 'providernum',
+                              select_label  => 'provider',
+                              disable_inventory => 1,
+                           },
+    'typenum'           => {
+                              label         => 'Circuit type',
+                              type          => 'select',
+                              select_table  => 'circuit_type',
+                              select_key    => 'typenum',
+                              select_label  => 'typename',
+                              disable_inventory => 1,
+                           },
+    'termnum'           => {
+                              label         => 'Termination type',
+                              type          => 'select',
+                              select_table  => 'circuit_termination',
+                              select_key    => 'termnum',
+                              select_label  => 'termination',
+                              disable_inventory => 1,
+                           },
+    'circuit_id'        => { label => 'Circuit ID', %dis },
+    'desired_due_date'  => { label => 'Desired due date',
+                             %dis
+                           },
+    'due_date'          => { label => 'Due date',
+                             %dis
+                           },
+    'vendor_order_id'   => { label => 'Vendor order ID', %dis },
+    'vendor_qual_id'    => { label => 'Vendor qualification ID', %dis },
+    'vendor_order_type' => {
+                              label => 'Vendor order type',
+                              disable_inventory => 1
+                           }, # should be a select?
+    'vendor_order_status' => {
+                              label => 'Vendor order status',
+                              disable_inventory => 1
+                             }, # should also be a select?
+    'endpoint_ip_addr'  => {
+                              label => 'Endpoint IP address',
+                           },
+    'endpoint_mac_addr' => {
+                              label => 'Endpoint MAC address',
+                              type => 'input-mac_addr',
+                              disable_inventory => 1,
+                           },
+  );
+  return {
+    'name'              => 'Circuit',
+    'name_plural'       => 'Circuits',
+    'longname_plural'   => 'Voice and data circuit services',
+    'display_weight'    => 72,
+    'cancel_weight'     => 85, # after svc_phone
+    'fields'            => \%fields,
+  };
+}
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid service.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $mac_addr = uc($self->get('endpoint_mac_addr'));
+  $mac_addr =~ s/[\W_]//g;
+  $self->set('endpoint_mac_addr', $mac_addr);
+
+  my $error = 
+    $self->ut_numbern('svcnum')
+    || $self->ut_number('typenum')
+    || $self->ut_number('providernum')
+    || $self->ut_text('circuit_id')
+    || $self->ut_numbern('desired_due_date')
+    || $self->ut_numbern('due_date')
+    || $self->ut_textn('vendor_order_id')
+    || $self->ut_textn('vendor_qual_id')
+    || $self->ut_textn('vendor_order_type')
+    || $self->ut_textn('vendor_order_status')
+    || $self->ut_ipn('endpoint_ip_addr')
+    || $self->ut_textn('endpoint_mac_addr')
+  ;
+
+  # no canonical values yet for vendor_order_status or _type
+
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=item label
+
+Returns the circuit ID.
+
+=cut
+
+sub label {
+  my $self = shift;
+  $self->get('circuit_id');
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/svc_phone.pm b/FS/FS/svc_phone.pm
index 4ca8d82..bd35cba 100644
--- a/FS/FS/svc_phone.pm
+++ b/FS/FS/svc_phone.pm
@@ -189,6 +189,14 @@ sub table_info {
                          select_label => 'domain',
                          disable_inventory => 1,
                        },
+        'circuit_svcnum'   => { label             => 'Circuit',
+                                type              => 'select',
+                                select_table      => 'svc_domain',
+                                select_key        => 'svcnum',
+                                select_label      => 'circuit_label',
+                                disable_inventory => 1,
+                              },
+
         'sms_carrierid'    => { label             => 'SMS Carrier',
                                 type              => 'select',
                                 select_table      => 'cdr_carrier',
@@ -711,6 +719,8 @@ sub radius_groups {
 
 =item sms_cdr_carrier
 
+Returns the L<FS::cdr_carrier> assigned as the SMS carrier for this phone.
+
 =cut
 
 sub sms_cdr_carrier {
@@ -721,6 +731,8 @@ sub sms_cdr_carrier {
 
 =item sms_carriername
 
+Returns the name of the SMS carrier, or an empty string if there isn't one.
+
 =cut
 
 sub sms_carriername {
@@ -729,6 +741,29 @@ sub sms_carriername {
   $cdr_carrier->carriername;
 }
 
+=item svc_circuit
+
+Returns the L<FS::svc_circuit> assigned as the trunk for this phone line.
+
+=item circuit_label
+
+Returns the label of the circuit (the part_svc label followed by the 
+circuit ID), or an empty string if there isn't one.
+
+=cut
+
+sub svc_circuit {
+  my $self = shift;
+  my $svcnum = $self->get('circuit_svcnum') or return '';
+  return FS::svc_circuit->by_key($svcnum);
+}
+
+sub circuit_label {
+  my $self = shift;
+  my $svc_circuit = $self->svc_circuit or return '';
+  return join(' ', $svc_circuit->part_svc->svc, $svc_circuit->circuit_id);
+}
+
 =item phone_device
 
 Returns any FS::phone_device records associated with this service.
diff --git a/FS/MANIFEST b/FS/MANIFEST
index 79a7dc5..105447c 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -821,3 +821,11 @@ FS/deploy_zone_vertex.pm
 t/deploy_zone_vertex.t
 FS/tax_status.pm
 t/tax_status.t
+FS/circuit_type.pm
+t/circuit_type.t
+FS/circuit_provider.pm
+t/circuit_provider.t
+FS/circuit_termination.pm
+t/circuit_termination.t
+FS/svc_circuit.pm
+t/svc_circuit.t
diff --git a/FS/t/circuit_provider.t b/FS/t/circuit_provider.t
new file mode 100644
index 0000000..753a156
--- /dev/null
+++ b/FS/t/circuit_provider.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::circuit_provider;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/circuit_termination.t b/FS/t/circuit_termination.t
new file mode 100644
index 0000000..6f51271
--- /dev/null
+++ b/FS/t/circuit_termination.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::circuit_termination;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/circuit_type.t b/FS/t/circuit_type.t
new file mode 100644
index 0000000..dbb6e0a
--- /dev/null
+++ b/FS/t/circuit_type.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::circuit_type;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_circuit.t b/FS/t/svc_circuit.t
new file mode 100644
index 0000000..7fefcc0
--- /dev/null
+++ b/FS/t/svc_circuit.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_circuit;
+$loaded=1;
+print "ok 1\n";
diff --git a/httemplate/browse/circuit_provider.html b/httemplate/browse/circuit_provider.html
new file mode 100644
index 0000000..12f6532
--- /dev/null
+++ b/httemplate/browse/circuit_provider.html
@@ -0,0 +1,11 @@
+<& elements/browse-simple.html,
+  'table'               => 'circuit_provider',
+  'title'               => 'Circuit providers',
+  'menubar'             => [ 'Circuit types' => 'circuit_type.html',
+                             'Circuit terminations' => 'circuit_termination.html'
+                           ],
+  'name_singular'       => 'provider',
+  'name_header'         => 'Provider name',
+  'name_col'            => 'provider',
+  'acl'                 => 'Configuration',
+&>
diff --git a/httemplate/browse/circuit_termination.html b/httemplate/browse/circuit_termination.html
new file mode 100644
index 0000000..830ccf7
--- /dev/null
+++ b/httemplate/browse/circuit_termination.html
@@ -0,0 +1,11 @@
+<& elements/browse-simple.html,
+  'table'               => 'circuit_termination',
+  'title'               => 'Circuit terminations',
+  'menubar'             => [ 'Circuit types' => 'circuit_type.html',
+                             'Circuit providers' => 'circuit_provider.html'
+                           ],
+  'name_singular'       => 'termination type',
+  'name_header'         => 'Termination type',
+  'name_col'            => 'termination',
+  'acl'                 => 'Configuration',
+&>
diff --git a/httemplate/browse/circuit_type.html b/httemplate/browse/circuit_type.html
new file mode 100644
index 0000000..a145d54
--- /dev/null
+++ b/httemplate/browse/circuit_type.html
@@ -0,0 +1,11 @@
+<& elements/browse-simple.html,
+  'table'               => 'circuit_type',
+  'title'               => 'Circuit types',
+  'menubar'             => [ 'Circuit providers' => 'circuit_provider.html',
+                             'Circuit terminations' => 'circuit_termination.html'
+                           ],
+  'name_singular'       => 'circuit type',
+  'name_header'         => 'Circuit type',
+  'name_col'            => 'typename',
+  'acl'                 => 'Configuration',
+&>
diff --git a/httemplate/browse/elements/browse-simple.html b/httemplate/browse/elements/browse-simple.html
new file mode 100644
index 0000000..cfa27e8
--- /dev/null
+++ b/httemplate/browse/elements/browse-simple.html
@@ -0,0 +1,57 @@
+<& browse.html,
+  'query'               => { 'table' => $table },
+  'count_query'         => "SELECT COUNT(*) FROM $table",
+  'header'              => [ '#', $opt{name_header} ],
+  'fields'              => [ $table_key, $opt{name_col} ],
+  'links'               => [ '', '' ],
+  'link_onclicks'       => [ '', $sub_edit_popup ],
+  'disableable'         => 1,
+  'disabled_statuspos'  => 2,
+  'html_init'           => $html_init,
+  %opt,
+&>
+<%doc>
+A simple wrapper around search/elements/search.html for browsing/editing
+tables that only have a primary key, a 'disabled' field, and one other column
+which is the object's name or description. Usage:
+
+<& browse-simple.html,
+  # required
+  'table'         => 'mytable',
+  'title'         => 'My Things',
+  'name_singular' => 'thing',
+  'name_col'      => 'thingname',
+  'name_header'   => 'Thing name'
+  'acl'           => 'Configure things',
+&>
+
+</%doc>
+<%init>
+my %opt = @_;
+
+my $table = delete $opt{table};
+my $name_singular = $opt{name_singular};
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right($opt{acl});
+
+my $table_key = dbdef->table($table)->primary_key;
+my $sub_edit_popup = sub {
+  my $pkey = $_[0]->get($table_key);
+  include('/elements/popup_link_onclick.html',
+    'action'      => $p."edit/$table.html?$pkey",
+    'actionlabel' => "Edit $name_singular",
+    'width'       => 350,
+    'height'      => 220,
+  );
+};
+
+my $html_init = include('/elements/popup_link.html',
+    'action'      => $p."edit/$table.html?",
+    'actionlabel' => "Add $name_singular",
+    'width'       => 350,
+    'height'      => 220,
+    'label'       => "Add a new $name_singular",
+) . '<BR>';
+
+</%init>
diff --git a/httemplate/docs/part_svc-table.html b/httemplate/docs/part_svc-table.html
index 8d3711d..5e8d9e5 100644
--- a/httemplate/docs/part_svc-table.html
+++ b/httemplate/docs/part_svc-table.html
@@ -23,6 +23,7 @@
         <LI><B>svc_broadband</B>: Wireless broadband
         <LI><B>svc_cable</B>: Cable
         <LI><B>svc_dish</B>: DISH Network
+        <LI><B>svc_circuit</B>: Phone circuits other than DSL
       </UL>
     </TD>
     <TD VALIGN="top">
diff --git a/httemplate/edit/circuit_provider.html b/httemplate/edit/circuit_provider.html
new file mode 100644
index 0000000..6c8dced
--- /dev/null
+++ b/httemplate/edit/circuit_provider.html
@@ -0,0 +1,21 @@
+<& elements/edit.html,
+  'popup'         => 1,
+  'table'         => 'circuit_provider',
+  'name_singular' => 'provider',
+  'labels'        => \%labels,
+  'fields'        => \@fields,
+&>
+<%init>
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my @fields = (
+  'provider',
+  { field => 'disabled', type => 'checkbox', value => 'Y' }
+);
+my %labels = (
+  'providernum' => '',
+  'provider' => 'Provider name',
+  'disabled' => 'Disabled'
+);
+</%init>
diff --git a/httemplate/edit/circuit_termination.html b/httemplate/edit/circuit_termination.html
new file mode 100644
index 0000000..0317bce
--- /dev/null
+++ b/httemplate/edit/circuit_termination.html
@@ -0,0 +1,21 @@
+<& elements/edit.html,
+  'popup'         => 1,
+  'table'         => 'circuit_termination',
+  'name_singular' => 'termination type',
+  'labels'        => \%labels,
+  'fields'        => \@fields,
+&>
+<%init>
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my @fields = (
+  'termination',
+  { field => 'disabled', type => 'checkbox', value => 'Y' }
+);
+my %labels = (
+  'termnum' => '',
+  'termination' => 'Termination type',
+  'disabled' => 'Disabled'
+);
+</%init>
diff --git a/httemplate/edit/circuit_type.html b/httemplate/edit/circuit_type.html
new file mode 100644
index 0000000..8977588
--- /dev/null
+++ b/httemplate/edit/circuit_type.html
@@ -0,0 +1,21 @@
+<& elements/edit.html,
+  'popup'         => 1,
+  'table'         => 'circuit_type',
+  'name_singular' => 'circuit type',
+  'labels'        => \%labels,
+  'fields'        => \@fields,
+&>
+<%init>
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my @fields = (
+  'typename',
+  { field => 'disabled', type => 'checkbox', value => 'Y' }
+);
+my %labels = (
+  'typenum' => '',
+  'typename' => 'Circuit type',
+  'disabled' => 'Disabled'
+);
+</%init>
diff --git a/httemplate/edit/elements/part_svc_column.html b/httemplate/edit/elements/part_svc_column.html
index 64901a8..6dcb602 100644
--- a/httemplate/edit/elements/part_svc_column.html
+++ b/httemplate/edit/elements/part_svc_column.html
@@ -249,7 +249,10 @@ that field.
   </TR>
 % }
 % # special case: services with attached routers (false laziness...)
-% if ( $svcdb eq 'svc_acct' or $svcdb eq 'svc_broadband' or $svcdb eq 'svc_dsl' ) {
+% if ( $svcdb eq 'svc_acct'
+%      or $svcdb eq 'svc_broadband'
+%      or $svcdb eq 'svc_dsl'
+%      or $svcdb eq 'svc_circuit' ) {
 %   push @fields, 'has_router';
   <TR>
     <TD COLSPAN=3 ALIGN="right">
diff --git a/httemplate/edit/elements/svc_Common.html b/httemplate/edit/elements/svc_Common.html
index fc29327..97b630f 100644
--- a/httemplate/edit/elements/svc_Common.html
+++ b/httemplate/edit/elements/svc_Common.html
@@ -103,10 +103,42 @@
                    my $flag = $columndef->columnflag;
 
                    if ( $flag eq 'F' ) { #fixed
-                     $f->{'type'} = length($columndef->columnvalue)
-                                      ? 'fixed'
-                                      : 'hidden';
                      $f->{'value'} = $columndef->columnvalue;
+                     if (length($columndef->columnvalue)) {
+
+                       if ( $f->{'type'} =~ /^select-?(.*)/ ) {
+                         # try to display this in a user-friendly manner
+                         if ( $f->{'table'} ) { # find matching records
+                           $f->{'value_col'} ||=
+                             dbdef->table($f->{'table'})->primary_key;
+
+                           my @values = split(',', $f->{'value'});
+                           my @recs;
+                           foreach (@values) {
+                             push @recs, qsearchs( $f->{'table'},
+                                         { $f->{'value_col'} => $_ }
+                                         );
+                           }
+                           if ( @recs ) {
+                             my $method = $f->{'name_col'};
+                             if ( $f->{'multiple'} ) {
+                               $f->{'formatted_value'} = [
+                                 map { $_->method } @recs
+                               ];
+                             } else { # there shouldn't be more than one...
+                               $f->{'formatted_value'} = $recs[0]->$method;
+                             }
+                           } # if not, then just let tr-fixed display the
+                             # values as-is
+
+                         } # other select types probably don't matter
+                       } # if it's a select
+
+                       $f->{'type'} = 'fixed';
+
+                     } else { # fixed, null
+                       $f->{'type'} = 'hidden';
+                     }
 
                    } elsif ( $flag eq 'A' ) { #auto assign from inventory
                      $f->{'type'} = 'hidden';
@@ -127,16 +159,14 @@
                                            };
 
                    } elsif ( $flag eq 'S' #selectable choice
-                               && $f->{type} !~ /^select-svc(-domain|_pbx)$/ ) {
+                               && $f->{type} !~ /^select-svc/ ) {
                      $f->{type}    = 'select';
                      $f->{options} = [ split( /\s*,\s*/,
                                                 $columndef->columnvalue)
                                      ];
-                   }
+                   } # shouldn't this be enforced for all 'S' fields?
 
-                   if (    $f->{'type'} eq 'select-svc_pbx'
-                        || $f->{'type'} eq 'select-svc-domain' 
-                      )
+                   if ( $f->{'type'} =~ /^select-svc/ )
                    {
                      $f->{'include_opt_callback'} =
                        sub { ( 'pkgnum'  => $pkgnum,
diff --git a/httemplate/edit/process/circuit_provider.html b/httemplate/edit/process/circuit_provider.html
new file mode 100644
index 0000000..0a91a17
--- /dev/null
+++ b/httemplate/edit/process/circuit_provider.html
@@ -0,0 +1,11 @@
+<& elements/process.html,
+  'table'         => 'circuit_provider',
+  'viewall_dir'   => 'browse',
+  'popup_reload'  => 'Updating',
+&>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+</%init>
diff --git a/httemplate/edit/process/circuit_termination.html b/httemplate/edit/process/circuit_termination.html
new file mode 100644
index 0000000..94d29c0
--- /dev/null
+++ b/httemplate/edit/process/circuit_termination.html
@@ -0,0 +1,11 @@
+<& elements/process.html,
+  'table'         => 'circuit_termination',
+  'viewall_dir'   => 'browse',
+  'popup_reload'  => 'Updating',
+&>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+</%init>
diff --git a/httemplate/edit/process/circuit_type.html b/httemplate/edit/process/circuit_type.html
new file mode 100644
index 0000000..58f461e
--- /dev/null
+++ b/httemplate/edit/process/circuit_type.html
@@ -0,0 +1,11 @@
+<& elements/process.html,
+  'table'         => 'circuit_type',
+  'viewall_dir'   => 'browse',
+  'popup_reload'  => 'Updating',
+&>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+</%init>
diff --git a/httemplate/edit/process/elements/svc_Common.html b/httemplate/edit/process/elements/svc_Common.html
index 55ecc5f..ca336a1 100644
--- a/httemplate/edit/process/elements/svc_Common.html
+++ b/httemplate/edit/process/elements/svc_Common.html
@@ -29,7 +29,7 @@ my $args_callback = sub {
       map { $_ => $cgi->param("router_$_") }
       qw( routernum routername blocknum )
     });
-   if (length($router->routername) == 0) {
+   if ($router->blocknum and length($router->routername) == 0) {
       #sensible default
       $router->set('routername', $svc->label);
     }
diff --git a/httemplate/edit/process/svc_circuit.html b/httemplate/edit/process/svc_circuit.html
new file mode 100644
index 0000000..d28f913
--- /dev/null
+++ b/httemplate/edit/process/svc_circuit.html
@@ -0,0 +1,11 @@
+<& elements/svc_Common.html,
+    table       => 'svc_circuit',
+    edit_ext    => 'html',
+    redirect    => popurl(3)."view/svc_circuit.html?",
+&>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Provision customer service'); #something else more specific?
+
+</%init>
diff --git a/httemplate/edit/svc_circuit.cgi b/httemplate/edit/svc_circuit.cgi
new file mode 100644
index 0000000..3f9bad5
--- /dev/null
+++ b/httemplate/edit/svc_circuit.cgi
@@ -0,0 +1,54 @@
+<& elements/svc_Common.html,
+              'table'              => 'svc_circuit',
+              'fields'             => \@fields,
+&>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Provision customer service'); #something else more specific?
+
+my $conf = new FS::Conf;
+my $date_format = $conf->config('date_format') || '%m/%d/%Y';
+
+my @fields = (
+  { field         => 'providernum',
+    type          => 'select-table',
+    table         => 'circuit_provider',
+    name_col      => 'provider',
+    disable_empty => 1,
+  },
+  { field         => 'typenum',
+    type          => 'select-table',
+    table         => 'circuit_type',
+    name_col      => 'typename',
+    disable_empty => 1,
+  },
+  { field         => 'termnum',
+    type          => 'select-table',
+    table         => 'circuit_termination',
+    name_col      => 'termination',
+    disable_empty => 1,
+  },
+  { field         => 'circuit_id',
+    size          => 40,
+  },
+  { field         => 'desired_due_date',
+    type          => 'input-date-field',
+  },
+  { field         => 'due_date',
+    type          => 'input-date-field',
+  },
+  'vendor_order_id',
+  'vendor_qual_id',
+  'vendor_order_status',
+  'endpoint_ip_addr',
+  { field         => 'endpoint_mac_addr',
+    type          => 'input-mac_addr',
+  },
+);
+
+# needed: a new_callback to migrate vendor quals over to circuits
+
+#my ($svc_new_callback, $svc_edit_callback, $svc_error_callback);
+
+</%init>
diff --git a/httemplate/edit/svc_phone.cgi b/httemplate/edit/svc_phone.cgi
index f858205..f9c0d40 100644
--- a/httemplate/edit/svc_phone.cgi
+++ b/httemplate/edit/svc_phone.cgi
@@ -2,17 +2,12 @@
      'table'            => 'svc_phone',
      'fields'           => [],
      'begin_callback'   => $begin_callback,
-     'svc_new_callback' => sub {
-       my( $cgi, $svc_x, $part_svc, $cust_pkg, $fields, $opt ) = @_;
-       $svc_x->locationnum($cust_pkg->locationnum) if $cust_pkg;
-     },
-     'svc_edit_callback' => sub {
-       my( $cgi, $svc_x, $part_svc, $cust_pkg, $fields, $opt) = @_;
-       my $conf = new FS::Conf;
-       $svc_x->sip_password('*HIDDEN*') unless $conf->exists('showpasswords');
-     },
+     'svc_new_callback'   => $svc_callback,
+     'svc_edit_callback'  => $svc_callback,
+     'svc_error_callback' => $svc_callback,
 &>
 <%init>
+my $conf = new FS::Conf;
 
 my $begin_callback = sub {
   my( $cgi, $fields, $opt ) = @_;
@@ -25,8 +20,6 @@ my $begin_callback = sub {
   die "access denied"
     unless $FS::CurrentUser::CurrentUser->access_right($right);
 
-  my $conf = new FS::Conf;
-
   push @$fields,
               'countrycode',
               { field    => 'phonenum',
@@ -149,7 +142,26 @@ my $begin_callback = sub {
 
   }
 
-};
+}; # begin_callback
 
+# svc_edit_callback / svc_new_callback
+my $svc_callback = sub {
+  my ($cgi, $svc_x, $part_svc, $cust_pkg, $fields, $opt) = @_;
 
+  push @$fields, {
+    field => 'circuit_svcnum',
+    type  => 'select-svc_circuit',
+    cust_pkg => $cust_pkg,
+    part_svc => $part_svc,
+  };
+
+  if ( $cust_pkg and not $svc_x->svcnum ) {
+    # new service, default to package location
+    $svc_x->set('locationnum', $cust_pkg->locationnum);
+  }
+
+  if ( not $conf->exists('showpasswords') and $svc_x->svcnum ) {
+    $svc_x->sip_password('*HIDDEN*');
+  }
+};
 </%init>
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index f26882b..b4ecdc4 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -552,6 +552,12 @@ tie my %config_conferencing, 'Tie::IxHash',
   'Quality levels'     => [ $fsurl.'browse/conferencing_quality.html', '' ],
 ;
 
+tie my %config_circuit, 'Tie::IxHash',
+  'Circuit types'     => [ $fsurl.'browse/circuit_type.html',         '' ],
+  'Circuit providers' => [ $fsurl.'browse/circuit_provider.html',     '' ],
+  'Termination types' => [ $fsurl.'browse/circuit_termination.html',  '' ],
+;
+
 tie my %config_export_svc, 'Tie::IxHash', ();
 if ( $curuser->access_right('Configuration') ) {
   $config_export_svc{'Service definitions'} = [ $fsurl.'browse/part_svc.cgi', 'Services are items you offer to your customers' ];
@@ -572,6 +578,8 @@ $config_export_svc{'Conferencing'} = [ \%config_conferencing, '' ]
   if $curuser->access_right('Configuration');
 $config_export_svc{'Alarm'} = [ \%config_alarm, '' ]
   if $curuser->access_right(['Alarm configuration', 'Alarm global configuration']);
+$config_export_svc{'Circuits'} = [ \%config_circuit, '' ]
+  if $curuser->access_right('Configuration');
 $config_export_svc{'Hardware types'} = [ $fsurl.'browse/hardware_class.html', 'Set up hardware type catalog' ]
   if $curuser->access_right('Configuration');
 
diff --git a/httemplate/elements/tr-select-svc_circuit.html b/httemplate/elements/tr-select-svc_circuit.html
new file mode 100644
index 0000000..fb55501
--- /dev/null
+++ b/httemplate/elements/tr-select-svc_circuit.html
@@ -0,0 +1,41 @@
+% if ( $columnflag eq 'F' ) { # no good reason for this, but support it anyway
+  <INPUT TYPE="hidden" NAME="circuit_svcnum" VALUE="<% $circuit_svcnum %>">
+% } else { 
+  <& tr-select-table.html,
+    'table'       => 'svc_circuit',
+    'name_col'    => 'circuit_id',
+    'empty_label' => ' ',
+    %select_hash,
+    %opt
+  &>
+% } 
+<%init>
+
+my %opt = @_;
+
+my $circuit_svcnum;
+if ( $opt{'curr_value'} =~ /^(\d+)$/ ) {
+  $circuit_svcnum = $1;
+}
+
+# generally not the svcpart of the circuit service (or any circuit service)
+my $part_svc = $opt{'part_svc'}
+               || qsearchs('part_svc', { 'svcpart' => $opt{'svcpart'} });
+
+my $columnflag = $part_svc->part_svc_column('circuit_svcnum')->columnflag;
+
+my $cust_pkg = $opt{'cust_pkg'};
+my $custnum;
+$custnum = $cust_pkg->custnum if $cust_pkg;
+
+my %select_hash;
+if ( $custnum =~ /^(\d+)$/ ) {
+  %select_hash = (
+    'addl_from' => ' LEFT JOIN cust_svc USING (svcnum)' .
+                   ' LEFT JOIN cust_pkg USING (pkgnum)',
+    'extra_sql' => " WHERE cust_pkg.custnum = $custnum".
+                   " OR svcnum = $circuit_svcnum",
+  );
+}
+
+</%init>
diff --git a/httemplate/search/svc_circuit.cgi b/httemplate/search/svc_circuit.cgi
new file mode 100755
index 0000000..c14c55f
--- /dev/null
+++ b/httemplate/search/svc_circuit.cgi
@@ -0,0 +1,65 @@
+<& elements/svc_Common.html,
+  'title'       => 'Circuit Search Results',
+  'name'        => 'circuit services',
+  'query'       => $query,
+  'count_query' => $query->{'count_query'},
+  'redirect'    => [ popurl(2). "view/svc_circuit.html?", 'svcnum' ],
+  'header'      => [ '#',
+                     'Provider',
+                     'Type',
+                     'Termination',
+                     'Circuit ID',
+                     'IP Address',
+                     FS::UI::Web::cust_header($cgi->param('cust_fields')),
+                   ],
+  'fields'      => [ 'svcnum',
+                     'provider',
+                     'typename',
+                     'termination',
+                     'circuit_id',
+                     'ip_addr',
+                     \&FS::UI::Web::cust_fields,
+                   ],
+  'links'       => [ $link,
+                     '',
+                     '',
+                     '',
+                     $link,
+                     $link,
+                     FS::UI::Web::cust_links($cgi->param('cust_fields')),
+                   ],
+  'align'       => 'rlllll'.  FS::UI::Web::cust_aligns(),
+  'color'       => [ 
+                     ('') x 6,
+                     FS::UI::Web::cust_colors(),
+                   ],
+  'style'       => [ 
+                     ('') x 6,
+                     FS::UI::Web::cust_styles(),
+                   ],
+
+&>
+<%init>
+
+die "access denied" unless
+  $FS::CurrentUser::CurrentUser->access_right('List services');
+
+my $conf = new FS::Conf;
+
+my %search_hash;
+if ( $cgi->param('magic') eq 'unlinked' ) {
+  %search_hash = ( 'unlinked' => 1 );
+} else {
+  foreach (qw( custnum agentnum svcpart cust_fields )) {
+    $search_hash{$_} = $cgi->param($_) if $cgi->param($_);
+  }
+  foreach (qw(pkgpart routernum towernum sectornum)) {
+    $search_hash{$_} = [ $cgi->param($_) ] if $cgi->param($_);
+  }
+}
+
+my $query = FS::svc_circuit->search(\%search_hash);
+
+my $link = [ $p.'view/svc_circuit.html?', 'svcnum' ];
+
+</%init>
diff --git a/httemplate/view/elements/svc_Common.html b/httemplate/view/elements/svc_Common.html
index b12d2dd..6c5c902 100644
--- a/httemplate/view/elements/svc_Common.html
+++ b/httemplate/view/elements/svc_Common.html
@@ -32,6 +32,15 @@ function areyousure(href) {
     window.location.href = href;
 }
 </SCRIPT>
+<STYLE>
+  td.content {
+    background-color: #ffffff;
+  }
+  .error {
+    color: #ff0000;
+    font-weight: bold;
+  }
+</STYLE>
 
 % if ( $custnum ) { 
 
@@ -67,61 +76,20 @@ function areyousure(href) {
 
 <% ntable("#cccccc") %><TR><TD><% ntable("#cccccc",2) %>
 
-% my @inventory_items = $svc_x->inventory_item;
 % foreach my $f ( @$fields ) {
-%
-%   my($field, $type, $value);
-%   if ( ref($f) ) {
-%     $field = $f->{'field'};
-%     $type  = $f->{'type'} || 'text';
-%     if ( $f->{'value_callback'} ) {
-%       my $hack_strict_refs = \&{ $f->{'value_callback'} };
-%       $value = &$hack_strict_refs($svc_x);
-%     } else {
-%       $value = encode_entities($svc_x->$field);
-%     }
-%   } else {
-%     $field = $f;
-%     $type = 'text';
-%     $value = encode_entities($svc_x->$field);
-%   }
-%
-%   my $columndef = $part_svc->part_svc_column($field);
-%   if ( $columndef->columnflag =~ /^[MA]$/ && $columndef->columnvalue =~ /,/ )
-%   {
-%     # inventory-select field with multiple classes
-%     # show the class name to disambiguate
-%     my ($item) = grep { $_->svc_field eq $field } @inventory_items;
-%     my $class = qsearchs('inventory_class', { classnum => $item->classnum });
-%     $value .= ' <i>('. $class->classname . ')</i>' if $class;
-%   }
-%   unless ($columndef->columnflag eq 'F' && !length($columndef->columnvalue)) {
-
+%   my ($field, $label, $value) = &{ $format_field }($f);
+%   next if !$field; 
       <TR>
         <TD ALIGN="right">
-          <% ( $opt{labels} && exists $opt{labels}->{$field} )
-                  ? $opt{labels}->{$field}
-                  : $field
-          %>
+          <% $label %>
         </TD>
 
-%	$value = time2str($date_format,$value)
-%         if $type eq 'date' && $value;
-%	$value = time2str("$date_format %H:%M",$value)
-%         if $type eq 'datetime' && $value;
-%	$value = $value eq 'Y' ? emt('Yes') : emt('No')
-%         if $type eq 'checkbox';
-%       $value .= ' ('. (Net::MAC::Vendor::lookup($value))->[0]. ')'
-%         if $type =~ /mac_addr$/ && $value =~ /\w/i;
-%       #eventually more options for <SELECT>, etc. fields
-
-        <TD BGCOLOR="#ffffff"><% $value %><TD>
-
+        <TD CLASS="content">
+          <% $value %>
+        </TD>
       </TR>
 
-%   }
-%
-% } 
+% }
 
 % foreach (sort { $a cmp $b } $svc_x->virtual_fields) { 
   <% $svc_x->pvf($_)->widget('HTML', 'view', $svc_x->getfield($_)) %>
@@ -193,7 +161,7 @@ my $svc_x = qsearchs({
 
 my $cust_svc = $svc_x->cust_svc;
 my ($label, $value, $svcdb, $part_svc );
-my $labels = $opt{labels}; #not -> here
+my $labels = $opt{labels} || {};
 
 if ( $cust_svc ) {
   ($label, $value, $svcdb) = $cust_svc->label;
@@ -227,7 +195,10 @@ if ($pkgnum) {
 
 # attached routers
 if ( my $router = qsearchs('router', { svcnum => $svc_x->svcnum }) ) {
-  push @$fields, qw(router_routername router_block);
+  push @$fields,
+    'router_routername',
+    'router_block';
+
   $labels->{'router_routername'} = 'Attached router';
   $labels->{'router_block'} = 'Attached address block';
   $svc_x->set('router_routername', $router->routername);
@@ -235,10 +206,100 @@ if ( my $router = qsearchs('router', { svcnum => $svc_x->svcnum }) ) {
   if ( $block ) {
     $svc_x->set('router_block', $block->cidr);
   } else {
-    $svc_x->set('router_block', '<i>(none)</i>');
+    $svc_x->set('router_block', '(none)');
   }
 }
 
+my @inventory_items = $svc_x->inventory_item;
+
+my $format_field = sub {
+  my $f = shift;
+  my($field, $type, $value);
+  if ( ref($f) ) {
+    $field = $f->{'field'};
+    $type  = $f->{'type'} || 'text';
+  } else {
+    $field = $f;
+    $type = 'text';
+  }
+
+  my $columndef = $part_svc->part_svc_column($field);
+  # skip fields that are fixed and empty
+  if ( $columndef->columnflag eq 'F'
+       and length($columndef->columnvalue) == 0 ) {
+    return;
+  }
+
+  # things that override the column value: value_callback, select
+  if ( ref($f) and $f->{'value_callback'} ) {
+
+    my $hack_strict_refs = \&{ $f->{'value_callback'} };
+    $value = &$hack_strict_refs($svc_x);
+
+  } elsif ( $type eq 'select-table' ) {
+    # imitates the /elements/select-table interface
+    $value = $svc_x->$field;
+
+    my $value_col = $f->{'value_col'} ||
+                    dbdef->table($f->{'table'})->primary_key;
+    my $name_col = $f->{'name_col'} or die 'name_col required';
+    # we don't yet support multiple-valued fields here
+    my $obj = qsearchs($f->{'table'}, { $value_col => $value });
+    if ( $obj ) {
+      $value = $obj->$name_col; # can be any method of the object
+    } else {
+      # show the raw value, but mark it as an error
+      $value = '<SPAN CLASS="error">' . $f->{'table'} . ' ' .
+                encode_entities($value) . '</SPAN>';
+    }
+
+  } else {
+    $value = encode_entities($svc_x->$field);
+  }
+
+  # inventory-select field with multiple classes
+  # show the class name to disambiguate
+  if ( $columndef->columnflag =~ /^[MA]$/ && $columndef->columnvalue =~ /,/ )
+  {
+    my ($item) = grep { $_->svc_field eq $field } @inventory_items;
+    my $class = qsearchs('inventory_class', { classnum => $item->classnum });
+    $value .= ' <i>('. $class->classname . ')</i>' if $class;
+  }
+
+  # formatting tweaks
+  if ( $type eq 'date' and $value ) {
+    $value = time2str($date_format,$value)
+  } elsif ( $type eq 'datetime' and $value ) {
+    $value = time2str("$date_format %H:%M",$value)
+  } elsif ( $type eq 'checkbox' ) {
+    $value = $value eq 'Y' ? emt('Yes') : emt('No');
+  } elsif ( $type eq 'mac_addr' and $value =~ /\w/) {
+    $value .= ' ('. (Net::MAC::Vendor::lookup($value))->[0]. ')'
+  }
+
+  # 'link' option
+  my $href;
+  if ( ref($f) and exists $f->{'link'} ) {
+    my $link = $f->{'link'};
+    if ( ref($link) eq 'CODE' ) {
+      $link = &{$link}($svc_x);
+    }
+    if ( ref($link) eq 'ARRAY' ) {
+      my ($base, $method) = @$link;
+      $href = $base . $svc_x->$method();
+    } elsif ( !ref($link) ) {
+      $href = $link;
+    }
+
+    if ( $href ) {
+      $value = qq!<A HREF="$href">$value</A>!;
+    }
+  }
+
+  my $label = $opt{labels}->{$field} || $field;
+  return ($field, $label, $value);
+};
+
 &{ $opt{'svc_callback'} }( $cgi, $svc_x, $part_svc, $cust_pkg, $fields, \%opt ) 
     if $opt{'svc_callback'};
 </%init>
diff --git a/httemplate/view/svc_circuit.html b/httemplate/view/svc_circuit.html
new file mode 100644
index 0000000..c8d5d23
--- /dev/null
+++ b/httemplate/view/svc_circuit.html
@@ -0,0 +1,80 @@
+<& elements/svc_Common.html,
+  'table'        => 'svc_circuit',
+  'labels'       => \%labels,
+  'fields'       => \@fields,
+  'html_foot'    => sub { $self->call_method('.foot', @_) },
+&>
+<%method .foot>
+% my $svc_circuit = shift;
+% my $link = [ 'svc_phone.cgi?', 'svcnum' ];
+% if ( FS::svc_phone->count('circuit_svcnum = '.$svc_circuit->svcnum) ) {
+<& /search/elements/search.html,
+
+  'title' => 'Provisioned phone services',
+  'name_singular' => 'phone number',
+  'query' => { 'table'      => 'svc_phone',
+               'hashref'    => { 'circuit_svcnum' => $svc_circuit->svcnum },
+               'addl_from'  => ' LEFT JOIN cust_svc USING (svcnum)'.
+                               ' LEFT JOIN part_svc USING (svcpart)',
+               'select'     => 'svc_phone.*, part_svc.*',
+             },
+  'count_query' => 'SELECT COUNT(*) FROM svc_phone WHERE circuit_svcnum = '.
+                    $svc_circuit->svcnum,
+  'header' => [ '#', 'Service', 'Phone number', ],
+  'fields' => [ 'svcnum', 'svc', 'phonenum' ],
+  'links'  => [ $link, $link, $link ],
+  'align'  => 'rlr',
+
+  'html_form' => '<SPAN CLASS="fsinnerbox-title">Phone services</SPAN>',
+  'nohtmlheader' => 1,
+  'disable_total' => 1,
+  'disable_maxselect' => 1,
+  'really_disable_download' => 1,
+&>
+  <BR>
+% }
+</%method>
+<%init>
+
+my @fields = (
+  'circuit_id',
+  { field     => 'providernum',
+    type      => 'select-table',
+    table     => 'circuit_provider',
+    name_col  => 'provider',
+  },
+  { field     => 'typenum',
+    type      => 'select-table',
+    table     => 'circuit_type',
+    name_col  => 'typename',
+  },
+  { field     => 'termnum',
+    type      => 'select-table',
+    table     => 'circuit_termination',
+    name_col  => 'termination',
+  },
+  qw( vendor_qual_id vendor_order_id vendor_order_type vendor_order_status
+      desired_due_date due_date
+      endpoint_ip_addr
+  ),
+  { field     => 'endpoint_mac_addr', type => 'mac_addr' },
+);
+
+
+my %labels = (
+  circuit_id          => 'Circuit ID',
+  providernum         => 'Provider',
+  typenum             => 'Circuit type',
+  termnum             => 'Termination',
+  vendor_qual_id      => 'Qualification ID',
+  vendor_order_id     => 'Order ID',
+  vendor_order_type   => 'Order type',
+  vendor_order_status => 'Order status',
+  desired_due_date    => 'Desired due date',
+  due_date            => 'Due date',
+  endpoint_ip_addr    => 'Endpoint IP address',
+  endpoint_mac_addr   => 'MAC address',
+);
+
+my $self = $m->request_comp;
+</%init>
diff --git a/httemplate/view/svc_phone.cgi b/httemplate/view/svc_phone.cgi
index 2a2ef24..1c0fb39 100644
--- a/httemplate/view/svc_phone.cgi
+++ b/httemplate/view/svc_phone.cgi
@@ -1,7 +1,7 @@
 <& elements/svc_Common.html,
               'table'     => 'svc_phone',
               'fields'    => \@fields,
-	          'labels'    => \%labels,
+              'labels'    => \%labels,
               'html_foot' => $html_foot,
 &>
 <%init>
@@ -53,6 +53,11 @@ if ( $conf->exists('svc_phone-lnp') ) {
   ;
 }
 
+$labels{circuit_label} = mt('Circuit');
+push @fields, { field => 'circuit_label',
+                link => [ $p.'view/svc_circuit.html?', 'circuit_svcnum' ]
+              };
+
 my $html_foot = sub {
   my $svc_phone = shift;
 

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/AccessRight.pm                               |    1 +
 FS/FS/Mason.pm                                     |    4 +
 FS/FS/Schema.pm                                    |   74 +++++++
 FS/FS/UI/Web.pm                                    |   19 +-
 FS/FS/addr_block.pm                                |   18 ++
 eg/table_template.pm => FS/FS/circuit_provider.pm  |   49 ++---
 .../FS/circuit_termination.pm                      |   52 ++---
 eg/table_template.pm => FS/FS/circuit_type.pm      |   52 ++---
 FS/FS/router.pm                                    |    7 +
 FS/FS/svc_circuit.pm                               |  230 ++++++++++++++++++++
 FS/FS/svc_phone.pm                                 |   35 +++
 FS/MANIFEST                                        |    8 +
 FS/t/{AccessRight.t => circuit_provider.t}         |    2 +-
 FS/t/{AccessRight.t => circuit_termination.t}      |    2 +-
 FS/t/{quotation.t => circuit_type.t}               |    2 +-
 FS/t/{AccessRight.t => svc_circuit.t}              |    2 +-
 httemplate/browse/circuit_provider.html            |   11 +
 httemplate/browse/circuit_termination.html         |   11 +
 httemplate/browse/circuit_type.html                |   11 +
 httemplate/browse/elements/browse-simple.html      |   57 +++++
 httemplate/docs/part_svc-table.html                |    1 +
 httemplate/edit/circuit_provider.html              |   21 ++
 httemplate/edit/circuit_termination.html           |   21 ++
 httemplate/edit/circuit_type.html                  |   21 ++
 httemplate/edit/elements/part_svc_column.html      |    5 +-
 httemplate/edit/elements/svc_Common.html           |   46 +++-
 .../{cdr_carrier.html => circuit_provider.html}    |    5 +-
 ...able_provider.html => circuit_termination.html} |    5 +-
 .../{cdr_carrier.html => circuit_type.html}        |    5 +-
 httemplate/edit/process/elements/svc_Common.html   |    2 +-
 .../{svc_hardware.html => svc_circuit.html}        |    9 +-
 httemplate/edit/svc_circuit.cgi                    |   54 +++++
 httemplate/edit/svc_phone.cgi                      |   36 ++-
 httemplate/elements/menu.html                      |    8 +
 httemplate/elements/tr-select-svc_circuit.html     |   41 ++++
 httemplate/search/svc_circuit.cgi                  |   65 ++++++
 httemplate/view/elements/svc_Common.html           |  163 +++++++++-----
 httemplate/view/svc_circuit.html                   |   80 +++++++
 httemplate/view/svc_phone.cgi                      |    7 +-
 39 files changed, 1049 insertions(+), 193 deletions(-)
 copy eg/table_template.pm => FS/FS/circuit_provider.pm (54%)
 copy eg/table_template.pm => FS/FS/circuit_termination.pm (50%)
 copy eg/table_template.pm => FS/FS/circuit_type.pm (50%)
 create mode 100644 FS/FS/svc_circuit.pm
 copy FS/t/{AccessRight.t => circuit_provider.t} (79%)
 copy FS/t/{AccessRight.t => circuit_termination.t} (77%)
 copy FS/t/{quotation.t => circuit_type.t} (97%)
 copy FS/t/{AccessRight.t => svc_circuit.t} (82%)
 create mode 100644 httemplate/browse/circuit_provider.html
 create mode 100644 httemplate/browse/circuit_termination.html
 create mode 100644 httemplate/browse/circuit_type.html
 create mode 100644 httemplate/browse/elements/browse-simple.html
 create mode 100644 httemplate/edit/circuit_provider.html
 create mode 100644 httemplate/edit/circuit_termination.html
 create mode 100644 httemplate/edit/circuit_type.html
 copy httemplate/edit/process/{cdr_carrier.html => circuit_provider.html} (56%)
 copy httemplate/edit/process/{cable_provider.html => circuit_termination.html} (56%)
 copy httemplate/edit/process/{cdr_carrier.html => circuit_type.html} (57%)
 copy httemplate/edit/process/{svc_hardware.html => svc_circuit.html} (50%)
 create mode 100644 httemplate/edit/svc_circuit.cgi
 create mode 100644 httemplate/elements/tr-select-svc_circuit.html
 create mode 100755 httemplate/search/svc_circuit.cgi
 create mode 100644 httemplate/view/svc_circuit.html




More information about the freeside-commits mailing list