Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorIvan Kohler <ivan@freeside.biz>
Thu, 4 Dec 2014 18:37:16 +0000 (10:37 -0800)
committerIvan Kohler <ivan@freeside.biz>
Thu, 4 Dec 2014 18:37:16 +0000 (10:37 -0800)
101 files changed:
FS/FS/AccessRight.pm
FS/FS/Mason.pm
FS/FS/Misc/Geo.pm
FS/FS/Report/FCC_477.pm
FS/FS/Schema.pm
FS/FS/Template_Mixin.pm
FS/FS/UI/Web.pm
FS/FS/addr_block.pm
FS/FS/cdr/cx3.pm
FS/FS/cdr/earthlink.pm [new file with mode: 0644]
FS/FS/cdr/thinktel.pm [new file with mode: 0644]
FS/FS/circuit_provider.pm [new file with mode: 0644]
FS/FS/circuit_termination.pm [new file with mode: 0644]
FS/FS/circuit_type.pm [new file with mode: 0644]
FS/FS/cust_credit.pm
FS/FS/cust_pkg.pm
FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm
FS/FS/part_event/Action/Mixin/credit_bill.pm [new file with mode: 0644]
FS/FS/part_event/Action/Mixin/credit_flat.pm [new file with mode: 0644]
FS/FS/part_event/Action/Mixin/credit_pkg.pm
FS/FS/part_event/Action/Mixin/credit_sales_pkg_class.pm
FS/FS/part_event/Action/bill_sales_credit.pm [new file with mode: 0644]
FS/FS/part_event/Action/bill_sales_credit_pkg_class.pm [new file with mode: 0644]
FS/FS/part_event/Action/pkg_agent_credit.pm
FS/FS/part_event/Action/pkg_agent_credit_pkg_class.pm
FS/FS/part_event/Action/pkg_employee_credit.pm
FS/FS/part_event/Action/pkg_referral_credit.pm
FS/FS/part_event/Action/pkg_sales_credit.pm
FS/FS/part_event/Action/pkg_sales_credit_pkg.pm
FS/FS/part_event/Action/pkg_sales_credit_pkg_class.pm
FS/FS/part_export/send_email.pm
FS/FS/pay_batch/RBC.pm
FS/FS/quotation_pkg.pm
FS/FS/reason.pm
FS/FS/router.pm
FS/FS/svc_circuit.pm [new file with mode: 0644]
FS/FS/svc_phone.pm
FS/MANIFEST
FS/t/circuit_provider.t [new file with mode: 0644]
FS/t/circuit_termination.t [new file with mode: 0644]
FS/t/circuit_type.t [new file with mode: 0644]
FS/t/svc_circuit.t [new file with mode: 0644]
bin/cdr-thinktel.import [new file with mode: 0755]
debian/rules
fs_selfservice/FS-SelfService/cgi/customer_order_pkg.html
fs_selfservice/FS-SelfService/cgi/do_process_forgot_password.html
httemplate/browse/circuit_provider.html [new file with mode: 0644]
httemplate/browse/circuit_termination.html [new file with mode: 0644]
httemplate/browse/circuit_type.html [new file with mode: 0644]
httemplate/browse/elements/browse-simple.html [new file with mode: 0644]
httemplate/browse/part_pkg-fcc.html
httemplate/docs/part_svc-table.html
httemplate/edit/circuit_provider.html [new file with mode: 0644]
httemplate/edit/circuit_termination.html [new file with mode: 0644]
httemplate/edit/circuit_type.html [new file with mode: 0644]
httemplate/edit/credit-cust_bill_pkg.html
httemplate/edit/cust_credit.cgi
httemplate/edit/cust_main/bottomfixup.js
httemplate/edit/elements/part_svc_column.html
httemplate/edit/elements/svc_Common.html
httemplate/edit/part_pkg.cgi
httemplate/edit/process/bulk-part_pkg-fcc.html
httemplate/edit/process/circuit_provider.html [new file with mode: 0644]
httemplate/edit/process/circuit_termination.html [new file with mode: 0644]
httemplate/edit/process/circuit_type.html [new file with mode: 0644]
httemplate/edit/process/credit-cust_bill_pkg.html
httemplate/edit/process/cust_credit.cgi
httemplate/edit/process/elements/svc_Common.html
httemplate/edit/process/part_event.html
httemplate/edit/process/svc_circuit.html [new file with mode: 0644]
httemplate/edit/svc_circuit.cgi [new file with mode: 0644]
httemplate/edit/svc_phone.cgi
httemplate/elements/input-fcc_options.html
httemplate/elements/location.html
httemplate/elements/menu.html
httemplate/elements/standardize_locations.js
httemplate/elements/tr-censustract.html
httemplate/elements/tr-input-fcc_options.html
httemplate/elements/tr-select-reason.html
httemplate/elements/tr-select-svc_circuit.html [new file with mode: 0644]
httemplate/misc/cancel_cust.html
httemplate/misc/cancel_pkg.html
httemplate/misc/confirm-censustract.html
httemplate/misc/cust_main-cancel.cgi
httemplate/misc/cust_main-suspend.cgi
httemplate/misc/part_pkg_fcc_options.html
httemplate/misc/process/cancel_pkg.html
httemplate/misc/process/elements/reason [new file with mode: 0644]
httemplate/misc/suspend_cust.html
httemplate/misc/xmlhttp-address_standardize.html
httemplate/search/477.html
httemplate/search/svc_circuit.cgi [new file with mode: 0755]
httemplate/view/cust_main.cgi
httemplate/view/cust_main/locations.html
httemplate/view/cust_main/packages/location.html
httemplate/view/cust_main/packages/status.html
httemplate/view/elements/svc_Common.html
httemplate/view/svc_circuit.html [new file with mode: 0644]
httemplate/view/svc_phone.cgi
rt/lib/RT/CustomFields.pm
rt/share/html/Elements/ShowCustomFields

index bad831a..92cede6 100644 (file)
@@ -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)',
index 900da10..d3e45df 100644 (file)
@@ -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 ) {
index e41ba5d..dbc383a 100644 (file)
@@ -6,8 +6,7 @@ use vars qw( $DEBUG @EXPORT_OK $conf );
 use LWP::UserAgent;
 use HTTP::Request;
 use HTTP::Request::Common qw( GET POST );
-use HTTP::Cookies;
-use HTML::TokeParser;
+use JSON;
 use URI::Escape 3.31;
 use Data::Dumper;
 use FS::Conf;
@@ -29,7 +28,7 @@ FS::Misc::Geo - routines to fetch geographic information
 
 =over 4
 
-=item get_censustract LOCATION YEAR
+=item get_censustract_ffiec LOCATION YEAR
 
 Given a location hash (see L<FS::location_Mixin>) and a census map year,
 returns a census tract code (consisting of state, county, and tract 
@@ -41,105 +40,65 @@ sub get_censustract_ffiec {
   my $class = shift;
   my $location = shift;
   my $year  = shift;
+  $year ||= 2013;
 
-  warn Dumper($location, $year) if $DEBUG;
+  if ( length($location->{country}) and uc($location->{country}) ne 'US' ) {
+    return '';
+  }
 
-  my $url = 'http://www.ffiec.gov/Geocode/default.aspx';
+  warn Dumper($location, $year) if $DEBUG;
 
-  my $return = {};
-  my $error = '';
+  # the old FFIEC geocoding service was shut down December 1, 2014.
+  # welcome to the future.
+  my $url = 'https://geomap.ffiec.gov/FFIECGeocMap/GeocodeMap1.aspx/GetGeocodeData';
+  # build the single-line query
+  my $single_line = join(', ', $location->{address1},
+                               $location->{city},
+                               $location->{state}
+                        );
+  my $hashref = { sSingleLine => $single_line, iCensusYear => $year };
+  my $request = POST( $url,
+    'Content-Type' => 'application/json; charset=utf-8',
+    'Accept' => 'application/json',
+    'Content' => encode_json($hashref)
+  );
 
-  my $ua = new LWP::UserAgent('cookie_jar' => HTTP::Cookies->new);
-  my $res = $ua->request( GET( $url ) );
+  my $ua = new LWP::UserAgent;
+  my $res = $ua->request( $request );
 
   warn $res->as_string
     if $DEBUG > 2;
 
   if (!$res->is_success) {
 
-    $error = $res->message;
-
-  } else {
-
-    my $content = $res->content;
-
-    my $p = new HTML::TokeParser \$content;
-    my $viewstate;
-    my $eventvalidation;
-    while (my $token = $p->get_tag('input') ) {
-      if ($token->[1]->{name} eq '__VIEWSTATE') {
-        $viewstate = $token->[1]->{value};
-      }
-      if ($token->[1]->{name} eq '__EVENTVALIDATION') {
-        $eventvalidation = $token->[1]->{value};
-      }
-      last if $viewstate && $eventvalidation;
-    }
-
-    if (!$viewstate or !$eventvalidation ) {
+    die "Census tract lookup error: ".$res->message;
 
-      $error = "either no __VIEWSTATE or __EVENTVALIDATION found";
-
-    } else {
-
-      my($zip5, $zip4) = split('-',$location->{zip});
-
-      $year ||= '2013';
-      my @ffiec_args = (
-        __VIEWSTATE => $viewstate,
-        __EVENTVALIDATION => $eventvalidation,
-        __VIEWSTATEENCRYPTED => '',
-        ddlbYear    => $year,
-        txtAddress  => $location->{address1},
-        txtCity     => $location->{city},  
-        ddlbState   => $location->{state},
-        txtZipCode  => $zip5,
-        btnSearch   => 'Search',
-      );
-      warn join("\n", @ffiec_args )
-        if $DEBUG > 1;
-
-      push @{ $ua->requests_redirectable }, 'POST';
-      $res = $ua->request( POST( $url, \@ffiec_args ) );
-      warn $res->as_string
-        if $DEBUG > 2;
-
-      unless ($res->code  eq '200') {
-
-        $error = $res->message;
-
-      } else {
-
-        my @id = qw( MSACode StateCode CountyCode TractCode );
-        $content = $res->content;
-        warn $res->content if $DEBUG > 2;
-        $p = new HTML::TokeParser \$content;
-        my $prefix = 'UcGeoResult11_lb';
-        my $compare =
-          sub { my $t=shift; scalar( grep { lc($t) eq lc("$prefix$_")} @id ) };
-
-        while (my $token = $p->get_tag('span') ) {
-          next unless ( $token->[1]->{id} && &$compare( $token->[1]->{id} ) );
-          $token->[1]->{id} =~ /^$prefix(\w+)$/;
-          $return->{lc($1)} = $p->get_trimmed_text("/span");
-        }
-
-        unless ( $return->{tractcode} ) {
-          warn "$error: $content ". Dumper($return) if $DEBUG;
-          $error = "No census tract found";
-        }
-        $return->{tractcode} .= ' '
-          unless $error || $JSON::VERSION >= 2; #broken JSON 1 workaround
+  }
 
-      } #unless ($res->code  eq '200')
+  local $@;
+  my $content = eval { decode_json($res->content) };
+  die "Census tract JSON error: $@\n" if $@;
 
-    } #unless ($viewstate)
+  if ( !exists $content->{d}->{sStatus} ) {
+    die "Census tract response is missing a status indicator.\nThis is an FFIEC problem.\n";
+  }
+  if ( $content->{d}->{sStatus} eq 'Y' ) {
+    # success
+    # this also contains the (partial) standardized address, correct zip 
+    # code, coordinates, etc., and we could get all of them, but right now
+    # we only want the census tract
+    my $tract = join('', $content->{d}->{sStateCode},
+                         $content->{d}->{sCountyCode},
+                         $content->{d}->{sTractCode});
+    return $tract;
 
-  } #unless ($res->code  eq '200')
+  } else {
 
-  die "FFIEC Geocoding error: $error\n" if $error;
+    my $error = $content->{d}->{sMsg}
+            ||  'FFIEC lookup failed, but with no status message.';
+    die "$error\n";
 
-  $return->{'statecode'} .  $return->{'countycode'} .  $return->{'tractcode'};
+  }
 }
 
 #sub get_district_methods {
index ff29d19..f5d6a06 100644 (file)
@@ -4,14 +4,13 @@ use base qw( FS::Report );
 use strict;
 use vars qw( @upload @download @technology @part2aoption @part2boption
              %states
-             $DEBUG
            );
 use FS::Record qw( dbh );
 
 use Tie::IxHash;
 use Storable;
 
-$DEBUG = 0;
+our $DEBUG = 0;
 
 =head1 NAME
 
@@ -305,6 +304,7 @@ sub report {
     unless $class->can($method);
   my $statement = $class->$method(%opt);
 
+  warn $statement if $DEBUG;
   my $sth = dbh->prepare($statement);
   $sth->execute or die $sth->errstr;
   $sth->fetchall_arrayref;
index 396c866..91dfc5d 100644 (file)
@@ -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' ],
+                          },
                         ],
     },
 
@@ -5881,6 +5886,7 @@ sub tables_hashref {
         'disabled',      'char',    'NULL', 1, '', '', 
         'unsuspend_pkgpart', 'int',  'NULL', '', '', '',
         'unsuspend_hold','char',    'NULL', 1, '', '',
+        'unused_credit', 'char',    'NULL', 1, '', '',
       ],
       'primary_key'  => 'reasonnum',
       'unique'       => [],
@@ -6526,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',     '',      '', '', '',
index 5af5b27..05972c0 100644 (file)
@@ -2729,6 +2729,8 @@ sub _items_cust_bill_pkg {
             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
             'description' => $description,
             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
+            'unit_amount'   => sprintf("%.2f", $cust_bill_pkg->unitsetup),
+            'quantity'    => $cust_bill_pkg->quantity,
             'preref_html' => ( $opt{preref_callback}
                                  ? &{ $opt{preref_callback} }( $cust_bill_pkg )
                                  : ''
@@ -2740,6 +2742,12 @@ sub _items_cust_bill_pkg {
             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
             'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
+            'unit_amount'   => sprintf("%.2f", $cust_bill_pkg->unitrecur),
+            'quantity'    => $cust_bill_pkg->quantity,
+           'preref_html' => ( $opt{preref_callback}
+                                 ? &{ $opt{preref_callback} }( $cust_bill_pkg )
+                                 : ''
+                             ),
           };
         }
 
index 99c3560..e138692 100644 (file)
@@ -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.
index 3e62a68..ba0f61d 100755 (executable)
@@ -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
index e5b5f03..8c84807 100644 (file)
@@ -43,6 +43,7 @@ sub { my ($cdr, $duration) = @_;
 },                     #duration
        skip(1),        # unknown
        'disposition',  # call status
+       'accountcode',  # AccountCode
 
   ],
 );
diff --git a/FS/FS/cdr/earthlink.pm b/FS/FS/cdr/earthlink.pm
new file mode 100644 (file)
index 0000000..0421ef9
--- /dev/null
@@ -0,0 +1,44 @@
+package FS::cdr::earthlink;
+
+use strict;
+use vars qw( @ISA %info $date);
+use Time::Local;
+use FS::cdr qw(_cdr_date_parser_maker _cdr_min_parser_maker);
+use Date::Parse;
+
+@ISA = qw(FS::cdr);
+
+%info = (
+  'name'          => 'Earthlink',
+  'weight'        => 120,
+  'header'        => 1,
+  'import_fields' => [
+
+       'accountcode',                  #Account number
+              skip(2),                 #SERVICE LOC / BILL NUMBER 
+       sub { my($cdr, $date) = @_;  
+       
+       },                              #date 
+       sub { my($cdr, $time) = @_;
+
+       my $datetime = $date. " ". $time;
+       $cdr->set('startdate', $datetime );
+        },                             #time
+       sub { my($cdr, $src) = @_;      
+       $src =~ s/\D//g;
+       $cdr->set('src', $src);
+       },                              #ORIG NUMBER
+       skip(2),                        #ORIG CITY/ORIGSTATE
+       sub { my($cdr, $dst) = @_;
+        $dst =~ s/\D//g;
+        $cdr->set('dst', $dst);
+        },                             #TERM NUMBER
+       skip(2),                        #TERM CITY / TERM STATE
+       _cdr_min_parser_maker,          #MINUTES
+  ],
+);
+
+sub skip { map {''} (1..$_[0]) }
+
+1;
+
diff --git a/FS/FS/cdr/thinktel.pm b/FS/FS/cdr/thinktel.pm
new file mode 100644 (file)
index 0000000..ddb2127
--- /dev/null
@@ -0,0 +1,42 @@
+package FS::cdr::thinktel;
+
+use strict;
+use base qw( FS::cdr );
+use FS::cdr qw( _cdr_date_parser_maker _cdr_min_parser_maker );
+
+our %info = (
+  'name'          => 'Thinktel',
+  'weight'        => 541,
+  'header'        => 1,     #0 default, set to 1 to ignore the first line, or
+                            # to higher numbers to ignore that number of lines
+  'type'          => 'csv', #csv (default), fixedlength or xls
+  'sep_char'      => ',',   #for csv, defaults to ,
+  'disabled'      => 0,     #0 default, set to 1 to disable
+
+  #listref of what to do with each field from the CDR, in order
+  'import_fields' => [
+    'charged_party',
+    'src',
+    'dst',
+    _cdr_date_parser_maker('startdate'),
+    'billsec', # rounded call duration
+    'dcontext', # Usage Type: 'Local', 'Canada', 'Incoming', ...
+    'upstream_price',
+    'upstream_src_regionname',
+    'upstream_dst_regionname',
+    '', # upstream rate per minute
+    '', # "Label"
+    # raw seconds, to one decimal place
+    sub { my ($cdr, $sec) = @_;
+          $cdr->set('duration', sprintf('%.0f', $sec));
+        },
+    # newly added fields of unclear meaning:
+    # Subscription (UUID, seems to correspond to charged_party)
+    # Call Type (always "Normal" thus far)
+    # Carrier (always empty)
+    # Alt Destination Name (always empty)
+  ],
+);
+
+1;
+
diff --git a/FS/FS/circuit_provider.pm b/FS/FS/circuit_provider.pm
new file mode 100644 (file)
index 0000000..6cb7841
--- /dev/null
@@ -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 (file)
index 0000000..3f0afc1
--- /dev/null
@@ -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 (file)
index 0000000..3b36536
--- /dev/null
@@ -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;
+
index 156ba5f..212be7a 100644 (file)
@@ -679,11 +679,9 @@ Example:
     'apply'             => 1, #0 leaves the credit unapplied
 
     #the credit
-    'newreasonnum'      => scalar($cgi->param('newreasonnum')),
-    'newreasonnum_type' => scalar($cgi->param('newreasonnumT')),
     map { $_ => scalar($cgi->param($_)) }
       #fields('cust_credit')  
-      qw( custnum _date amount reason reasonnum addlinfo ), #pkgnum eventnum
+      qw( custnum _date amount reasonnum addlinfo ), #pkgnum eventnum
 
   );
 
@@ -725,26 +723,11 @@ sub credit_lineitems {
   #});
 
   my $error = '';
-  if ($arg{reasonnum} == -1) {
-
-    $error = 'Enter a new reason (or select an existing one)'
-      unless $arg{newreasonnum} !~ /^\s*$/;
-    my $reason = new FS::reason {
-                   'reason'      => $arg{newreasonnum},
-                   'reason_type' => $arg{newreasonnum_type},
-                 };
-    $error ||= $reason->insert;
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return "Error inserting reason: $error";
-    }
-    $arg{reasonnum} = $reason->reasonnum;
-  }
 
   my $cust_credit = new FS::cust_credit ( {
     map { $_ => $arg{$_} }
       #fields('cust_credit')
-      qw( custnum _date amount reason reasonnum addlinfo ), #pkgnum eventnum
+      qw( custnum _date amount reasonnum addlinfo ), #pkgnum eventnum
   } );
   $error = $cust_credit->insert;
   if ( $error ) {
index e8e202e..a810f5a 100644 (file)
@@ -1207,7 +1207,7 @@ Available options are:
 
 =over 4
 
-=item reason - can be set to a cancellation reason (see L<FS:reason>), 
+=item reason - can be set to a cancellation reason (see L<FS:reason>),
 either a reasonnum of an existing reason, or passing a hashref will create 
 a new reason.  The hashref should have the following keys: 
 - typenum - Reason type (see L<FS::reason_type>
@@ -1297,6 +1297,16 @@ sub suspend {
     }
   }
 
+  # if a reasonnum was passed, get the actual reason object so we can check
+  # unused_credit
+  # (passing a reason hashref is still allowed, but it can't be used with
+  # the fancy behavioral options.)
+
+  my $reason;
+  if ($options{'reason'} =~ /^\d+$/) {
+    $reason = FS::reason->by_key($options{'reason'});
+  }
+
   my %hash = $self->hash;
   if ( $date ) {
     $hash{'adjourn'} = $date;
@@ -1321,9 +1331,15 @@ sub suspend {
     return $error;
   }
 
-  unless ( $date ) {
+  unless ( $date ) { # then we are suspending now
+
     # credit remaining time if appropriate
-    if ( $self->part_pkg->option('unused_credit_suspend', 1) ) {
+    # (if required by the package def, or the suspend reason)
+    my $unused_credit = $self->part_pkg->option('unused_credit_suspend',1)
+                        || ( defined($reason) && $reason->unused_credit );
+
+    if ( $unused_credit ) {
+      warn "crediting unused time on pkg#".$self->pkgnum."\n" if $DEBUG;
       my $error = $self->credit_remaining('suspend', $suspend_time);
       if ($error) {
         $dbh->rollback if $oldAutoCommit;
@@ -3872,7 +3888,7 @@ sub insert_reason {
     $reasonnum = $reason->reasonnum;
 
   } else {
-    return "Unparsable reason: ". $options{'reason'};
+    return "Unparseable reason: ". $options{'reason'};
   }
 
   my $cust_pkg_reason =
index cb61f1b..488132a 100644 (file)
@@ -1,21 +1,16 @@
 package FS::part_event::Action::Mixin::credit_agent_pkg_class;
-use base qw( FS::part_event::Action::Mixin::credit_pkg );
+
+# calculates a credit percentage on a specific package for use with 
+# credit_pkg or credit_bill, based on an agent's commission table
 
 use strict;
 use FS::Record qw(qsearchs);
 
-sub option_fields {
-  my $class = shift;
-  my %option_fields = $class->SUPER::option_fields;
-  delete $option_fields{'percent'};
-  %option_fields;
-}
-
 sub _calc_credit_percent {
-  my( $self, $cust_pkg ) = @_;
+  my( $self, $cust_pkg, $agent ) = @_;
 
   my $agent_pkg_class = qsearchs( 'agent_pkg_class', {
-    'agentnum' => $self->cust_main($cust_pkg)->agentnum,
+    'agentnum' => $agent->agentnum,
     'classnum' => $cust_pkg->part_pkg->classnum,
   });
 
diff --git a/FS/FS/part_event/Action/Mixin/credit_bill.pm b/FS/FS/part_event/Action/Mixin/credit_bill.pm
new file mode 100644 (file)
index 0000000..4930e35
--- /dev/null
@@ -0,0 +1,95 @@
+package FS::part_event::Action::Mixin::credit_bill;
+
+use strict;
+
+# credit_bill: calculates a credit amount that is some percentage of each 
+# line item of an invoice
+
+sub eventtable_hashref {
+  { 'cust_bill' => 1 };
+}
+
+sub option_fields {
+  my $class = shift;
+  my @fields = (
+    'reasonnum' => { 'label'        => 'Credit reason',
+                     'type'         => 'select-reason',
+                     'reason_class' => 'R',
+                   },
+    'percent'   => { 'label'   => 'Percent',
+                     'type'    => 'input-percentage',
+                     'default' => '100',
+                   },
+    'what' => {
+      'label'   => 'Of',
+      'type'    => 'select',
+      #add additional ways to specify in the package def
+      'options' => [qw( setuprecur setup recur setuprecur_margin setup_margin recur_margin )],
+      'labels'  => {
+        'setuprecur'        => 'Amount charged',
+        'setup'             => 'Setup fee',
+        'recur'             => 'Recurring fee',
+        'setuprecur_margin' => 'Amount charged minus total cost',
+        'setup_margin'      => 'Setup fee minus setup cost',
+        'recur_margin'      => 'Recurring fee minus recurring cost',
+      },
+    },
+  );
+  if ($class->can('_calc_credit_percent')) {
+    splice @fields, 2, 2; #remove the percentage option
+  }
+  @fields;
+    
+}
+
+our %part_pkg_cache;
+
+# arguments:
+# 1. the line item
+# 2. the recipient of the commission; may be FS::sales, FS::agent, 
+# FS::access_user, etc. Here we don't use it, but it will be passed through
+# to _calc_credit_percent.
+
+sub _calc_credit {
+  my $self = shift;
+  my $cust_bill_pkg = shift;
+
+  my $what = $self->option('what');
+  my $margin = 1 if $what =~ s/_margin$//;
+
+  my $pkgnum = $cust_bill_pkg->pkgnum;
+  my $cust_pkg = $cust_bill_pkg->cust_pkg;
+
+  my $percent;
+  if ( $self->can('_calc_credit_percent') ) {
+    $percent = $self->_calc_credit_percent($cust_pkg, @_);
+  } else {
+    $percent = $self->option('percent') || 0;
+  }
+
+  my $charge = 0;
+  if ( $what eq 'setup' ) {
+    $charge = $cust_bill_pkg->get('setup');
+  } elsif ( $what eq 'recur' ) {
+    $charge = $cust_bill_pkg->get('recur');
+  } elsif ( $what eq 'setuprecur' ) {
+    $charge = $cust_bill_pkg->get('setup') + $cust_bill_pkg->get('recur');
+  }
+  if ( $margin ) {
+    my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
+    my $part_pkg   = $part_pkg_cache{$pkgpart}
+                 ||= FS::part_pkg->by_key($pkgpart);
+    if ( $what eq 'setup' ) {
+      $charge -= $part_pkg->get('setup_cost');
+    } elsif ( $what eq 'recur' ) {
+      $charge -= $part_pkg->get('recur_cost');
+    } elsif ( $what eq 'setuprecur' ) {
+      $charge -= $part_pkg->get('setup_cost') + $part_pkg->get('recur_cost');
+    }
+  }
+
+  $charge = 0 if $charge < 0; # e.g. prorate
+  return ($percent * $charge / 100);
+}
+
+1;
diff --git a/FS/FS/part_event/Action/Mixin/credit_flat.pm b/FS/FS/part_event/Action/Mixin/credit_flat.pm
new file mode 100644 (file)
index 0000000..374cf5d
--- /dev/null
@@ -0,0 +1,25 @@
+package FS::part_event::Action::Mixin::credit_flat;
+
+# credit_flat: return a fixed amount for _calc_credit, specified in the 
+# options
+
+use strict;
+
+sub option_fields {
+  (
+    'reasonnum' => { 'label'        => 'Credit reason',
+                     'type'         => 'select-reason',
+                     'reason_class' => 'R',
+                   },
+    'amount'    => { 'label'        => 'Credit amount',
+                     'type'         => 'money',
+                   },
+  );
+}
+
+sub _calc_credit {
+  my $self = shift;
+  $self->option('amount');
+}
+
+1;
index e586f85..400ece9 100644 (file)
@@ -2,12 +2,19 @@ package FS::part_event::Action::Mixin::credit_pkg;
 
 use strict;
 
+# credit_pkg: calculates a credit amount that is some percentage of the 
+# package charge / cost / margin / some other amount of a package
+#
+# also provides an option field for the percentage, unless the action knows
+# how to calculate its own percentage somehow (has a _calc_credit_percent)
+
 sub eventtable_hashref {
   { 'cust_pkg' => 1 };
 }
 
 sub option_fields {
-  ( 
+  my $class = shift;
+  my @fields = (
     'reasonnum' => { 'label'        => 'Credit reason',
                      'type'         => 'select-reason',
                      'reason_class' => 'R',
@@ -36,12 +43,19 @@ sub option_fields {
       },
     },
   );
+  if ($class->can('_calc_credit_percent')) {
+    splice @fields, 2, 2; #remove the percentage option
+  }
+  @fields;
 }
 
-#my %no_cust_pkg = ( 'setup_cost' => 1 );
+# arguments:
+# 1. cust_pkg
+# 2. recipient of the credit (passed through to _calc_credit_percent)
 
 sub _calc_credit {
-  my( $self, $cust_pkg ) = @_;
+  my $self = shift;
+  my $cust_pkg = shift;
 
   my $cust_main = $self->cust_main($cust_pkg);
 
@@ -59,18 +73,17 @@ sub _calc_credit {
     }
   }
 
-  my $percent = $self->_calc_credit_percent($cust_pkg);
+  my $percent;
+  if ( $self->can('_calc_credit_percent') ) {
+    $percent = $self->_calc_credit_percent($cust_pkg, @_);
+  } else {
+    $percent = $self->option('percent') || 0;
+  }
 
-  #my @arg = $no_cust_pkg{$what} ? () : ($cust_pkg);
   my @arg = ($what eq 'setup_cost') ? () : ($cust_pkg);
 
   sprintf('%.2f', $part_pkg->$what(@arg) * $percent / 100 );
 
 }
 
-sub _calc_credit_percent {
-  my( $self, $cust_pkg ) = @_;
-  $self->option('percent');
-}
-
 1;
index 5c090ef..61302aa 100644 (file)
@@ -1,30 +1,16 @@
 package FS::part_event::Action::Mixin::credit_sales_pkg_class;
-use base qw( FS::part_event::Action::Mixin::credit_pkg );
 
 use strict;
 use FS::Record qw(qsearchs);
 use FS::sales_pkg_class;
 
-sub option_fields {
-  my $class = shift;
-  my %option_fields = $class->SUPER::option_fields;
-
-  delete $option_fields{'percent'};
-
-  %option_fields;
-}
-
 sub _calc_credit_percent {
-  my( $self, $cust_pkg ) = @_;
-
-  my $salesnum = $cust_pkg->salesnum;
-  $salesnum ||= $self->cust_main($cust_pkg)->salesnum
-    if $self->option('cust_main_sales');
+  my( $self, $cust_pkg, $sales ) = @_;
 
-  return 0 unless $salesnum;
+  die "sales record required" unless $sales;
 
   my $sales_pkg_class = qsearchs( 'sales_pkg_class', {
-    'salesnum' => $salesnum,
+    'salesnum' => $sales->salesnum,
     'classnum' => $cust_pkg->part_pkg->classnum,
   });
 
diff --git a/FS/FS/part_event/Action/bill_sales_credit.pm b/FS/FS/part_event/Action/bill_sales_credit.pm
new file mode 100644 (file)
index 0000000..3193a81
--- /dev/null
@@ -0,0 +1,91 @@
+package FS::part_event::Action::bill_sales_credit;
+
+# in this order:
+# - pkg_sales_credit invokes NEXT, then appends the 'cust_main_sales' param
+# - credit_bill contains the core _calc_credit logic, and also defines other
+# params
+
+use base qw( FS::part_event::Action::Mixin::pkg_sales_credit
+             FS::part_event::Action::Mixin::credit_bill
+             FS::part_event::Action );
+use FS::Record qw(qsearch qsearchs);
+use FS::Conf;
+use Date::Format qw(time2str);
+
+use strict;
+
+sub description { 'Credit the sales person based on the billed amount'; }
+
+sub eventtable_hashref {
+  { 'cust_bill' => 1 };
+}
+
+our $date_format;
+
+sub do_action {
+  my( $self, $cust_bill, $cust_event ) = @_;
+
+  $date_format ||= FS::Conf->new->config('date_format') || '%x';
+
+  my $cust_main = $self->cust_main($cust_bill);
+
+  my %salesnum_sales; # salesnum => FS::sales object
+  my %salesnum_amount; # salesnum => credit amount
+  my %pkgnum_pkg; # pkgnum => FS::cust_pkg
+  my %salesnum_pkgnums; # salesnum => [ pkgnum, ... ]
+
+  my @items = qsearch('cust_bill_pkg', { invnum => $cust_bill->invnum,
+                                         pkgnum => { op => '>', value => '0' }
+                                       });
+
+  foreach my $cust_bill_pkg (@items) {
+    my $pkgnum = $cust_bill_pkg->pkgnum;
+    my $cust_pkg = $pkgnum_pkg{$pkgnum} ||= $cust_bill_pkg->cust_pkg;
+
+    my $salesnum = $cust_pkg->salesnum;
+    $salesnum ||= $cust_main->salesnum
+      if $self->option('cust_main_sales');
+    my $sales = $salesnum_sales{$salesnum}
+            ||= FS::sales->by_key($salesnum);
+
+    next if !$sales; #no sales person, no credit
+
+    my $amount = $self->_calc_credit($cust_bill_pkg, $sales);
+
+    if ($amount > 0) {
+      $salesnum_amount{$salesnum} ||= 0;
+      $salesnum_amount{$salesnum} += $amount;
+      push @{ $salesnum_pkgnums{$salesnum} ||= [] }, $pkgnum;
+    }
+  }
+
+  foreach my $salesnum (keys %salesnum_amount) {
+    my $amount = sprintf('%.2f', $salesnum_amount{$salesnum});
+    next if $amount < 0.005;
+
+    my $sales = $salesnum_sales{$salesnum};
+
+    my $sales_cust_main = $sales->sales_cust_main;
+    die "No customer record for sales person ". $sales->salesperson
+      unless $sales->sales_custnum;
+
+    my $reasonnum = $self->option('reasonnum');
+
+    my $desc = 'from invoice #'. $cust_bill->display_invnum .
+               ' ('. time2str($date_format, $cust_bill->_date) . ')';
+               # could also show custnum and pkgnums here?
+    my $error = $sales_cust_main->credit(
+      $amount, 
+      \$reasonnum,
+      'eventnum'            => $cust_event->eventnum,
+      'addlinfo'            => $desc,
+      'commission_salesnum' => $sales->salesnum,
+    );
+    die "Error crediting customer ". $sales_cust_main->custnum.
+        " for sales commission: $error"
+      if $error;
+  } # foreach $salesnum
+
+}
+
+1;
diff --git a/FS/FS/part_event/Action/bill_sales_credit_pkg_class.pm b/FS/FS/part_event/Action/bill_sales_credit_pkg_class.pm
new file mode 100644 (file)
index 0000000..91442b9
--- /dev/null
@@ -0,0 +1,11 @@
+package FS::part_event::Action::bill_sales_credit_pkg_class;
+
+use base qw( FS::part_event::Action::Mixin::pkg_sales_credit
+             FS::part_event::Action::Mixin::credit_bill
+             FS::part_event::Action::Mixin::credit_sales_pkg_class
+             FS::part_event::Action::bill_sales_credit
+             );
+
+sub description { "Credit the sales person based on their commission percentage for the package's class"; }
+
+1;
index 494c40e..65f8c27 100644 (file)
@@ -1,7 +1,8 @@
 package FS::part_event::Action::pkg_agent_credit;
 
 use strict;
-use base qw( FS::part_event::Action::pkg_referral_credit );
+use base qw( FS::part_event::Action::Mixin::credit_flat
+             FS::part_event::Action );
 
 sub description { 'Credit the agent a specific amount'; }
 
@@ -18,7 +19,7 @@ sub do_action {
   my $agent_cust_main = $agent->agent_cust_main;
     #? or return "No customer record for agent ". $agent->agent;
 
-  my $amount = $self->_calc_credit($cust_pkg);
+  my $amount = $self->_calc_credit($cust_pkg, $agent);
   return '' unless $amount > 0;
 
   my $reasonnum = $self->option('reasonnum');
index 3dcf668..92c1556 100644 (file)
@@ -1,7 +1,8 @@
 package FS::part_event::Action::pkg_agent_credit_pkg_class;
 
 use strict;
-use base qw( FS::part_event::Action::Mixin::credit_agent_pkg_class
+use base qw( FS::part_event::Action::Mixin::credit_pkg
+             FS::part_event::Action::Mixin::credit_agent_pkg_class
              FS::part_event::Action::pkg_agent_credit );
 
 sub description { 'Credit the agent an amount based on their commission percentage for the referred package class'; }
index 64dd8b2..6cbe9bc 100644 (file)
@@ -1,7 +1,8 @@
 package FS::part_event::Action::pkg_employee_credit;
 
 use strict;
-use base qw( FS::part_event::Action::pkg_referral_credit );
+use base qw( FS::part_event::Action::Mixin::credit_flat
+             FS::part_event::Action );
 
 sub description { 'Credit the ordering employee a specific amount'; }
 
@@ -18,7 +19,7 @@ sub do_action {
   my $employee_cust_main = $employee->user_cust_main;
     #? or return "No customer record for employee ". $employee->username;
 
-  my $amount    = $self->_calc_credit($cust_pkg);
+  my $amount    = $self->_calc_credit($cust_pkg, $employee);
   return '' unless $amount > 0;
 
   my $reasonnum = $self->option('reasonnum');
index e7c92d6..9d7bbf8 100644 (file)
@@ -1,7 +1,8 @@
 package FS::part_event::Action::pkg_referral_credit;
 
 use strict;
-use base qw( FS::part_event::Action );
+use base qw( FS::part_event::Action::Mixin::credit_flat
+             FS::part_event::Action  );
 
 sub description { 'Credit the referring customer a specific amount'; }
 
@@ -9,19 +10,6 @@ sub eventtable_hashref {
   { 'cust_pkg' => 1 };
 }
 
-sub option_fields {
-  ( 
-    'reasonnum' => { 'label'        => 'Credit reason',
-                     'type'         => 'select-reason',
-                     'reason_class' => 'R',
-                   },
-    'amount'    => { 'label'        => 'Credit amount',
-                     'type'         => 'money',
-                   },
-  );
-
-}
-
 sub do_action {
   my( $self, $cust_pkg, $cust_event ) = @_;
 
@@ -35,7 +23,7 @@ sub do_action {
   return 'Referring customer is cancelled'
     if $referring_cust_main->status eq 'cancelled';
 
-  my $amount    = $self->_calc_credit($cust_pkg);
+  my $amount    = $self->_calc_credit($cust_pkg, $referring_cust_main);
   return '' unless $amount > 0;
 
   my $reasonnum = $self->option('reasonnum');
@@ -53,10 +41,4 @@ sub do_action {
 
 }
 
-sub _calc_credit {
-  my( $self, $cust_pkg ) = @_;
-
-  $self->option('amount');
-}
-
 1;
index e7551cd..3c569ca 100644 (file)
@@ -1,12 +1,15 @@
 package FS::part_event::Action::pkg_sales_credit;
-use base qw( FS::part_event::Action::Mixin::pkg_sales_credit
-             FS::part_event::Action::pkg_referral_credit );
+use base qw( FS::part_event::Action::Mixin::credit_flat
+             FS::part_event::Action );
 
 use strict;
 
 sub description { 'Credit the sales person a specific amount'; }
 
-#a little false laziness w/pkg_referral_credit
+sub eventtable_hashref {
+  { 'cust_pkg' => 1 };
+} 
+
 sub do_action {
   my( $self, $cust_pkg, $cust_event ) = @_;
 
@@ -24,7 +27,7 @@ sub do_action {
   my $sales_cust_main = $sales->sales_cust_main;
     #? or return "No customer record for sales person ". $sales->salesperson;
 
-  my $amount = $self->_calc_credit($cust_pkg);
+  my $amount = $self->_calc_credit($cust_pkg, $sales);
   return '' unless $amount > 0;
 
   my $reasonnum = $self->option('reasonnum');
index 9b13cd8..bd165f1 100644 (file)
@@ -1,4 +1,6 @@
 package FS::part_event::Action::pkg_sales_credit_pkg;
+
+# yes, they must be in this order
 use base qw( FS::part_event::Action::Mixin::pkg_sales_credit
              FS::part_event::Action::Mixin::credit_pkg
              FS::part_event::Action::pkg_sales_credit );
index c69c004..53ffc6c 100644 (file)
@@ -1,8 +1,10 @@
 package FS::part_event::Action::pkg_sales_credit_pkg_class;
 
 use base qw( FS::part_event::Action::Mixin::pkg_sales_credit
+             FS::part_event::Action::Mixin::credit_pkg
              FS::part_event::Action::Mixin::credit_sales_pkg_class
-             FS::part_event::Action::pkg_sales_credit );
+             FS::part_event::Action::pkg_sales_credit
+             );
 
 sub description { "Credit the package sales person an amount based on their commission percentage for the package's class"; }
 
index 41f0409..3e51422 100644 (file)
@@ -20,7 +20,8 @@ my %template_select = (
     %templates = (0 => '',
       map { $_->msgnum, $_->msgname } 
       qsearch({ table => 'msg_template',
-                hashref => { disabled => 1 },
+                hashref => { disabled => { 'op'    => '!=',
+                                           'value' => 1 }},
                 order_by => 'ORDER BY msgnum ASC'
               })
     );
index 4b11fdb..a5c4683 100644 (file)
@@ -108,7 +108,7 @@ $name = 'RBC';
     sprintf("%3s",$trans_code).
     sprintf("%10s",$client_num).
     ' '.
-    sprintf("%-19s", $cust_pay_batch->paybatchnum).
+    sprintf("%-19s", $cust_pay_batch->cust_main->custnum).
     '00'.
     sprintf("%04s", $bankno).
     sprintf("%05s", $branch).
index 79cce80..33c761e 100644 (file)
@@ -164,6 +164,26 @@ sub recur {
   sprintf('%.2f', $recur);
 }
 
+sub unitsetup {
+  my $self = shift;
+  return '0.00' if $self->waive_setup eq 'Y' || $self->{'_NO_SETUP_KLUDGE'};
+  my $part_pkg = $self->part_pkg;
+  my $setup = $part_pkg->option('setup_fee');
+
+  #XXX discounts
+  sprintf('%.2f', $setup);
+}
+
+sub unitrecur {
+  my $self = shift;
+  return '0.00' if $self->{'_NO_RECUR_KLUDGE'};
+  my $part_pkg = $self->part_pkg;
+  my $recur = $part_pkg->can('base_recur') ? $part_pkg->base_recur
+                                           : $part_pkg->option('recur_fee');
+  #XXX discounts
+  sprintf('%.2f', $recur);
+}
+
 =item part_pkg_currency_option OPTIONNAME
 
 Returns a two item list consisting of the currency of this quotation's customer
index e6b20db..f28989a 100644 (file)
@@ -56,6 +56,10 @@ suspensions but not others.
 whether to bill the unsuspend package immediately ('') or to wait until 
 the customer's next invoice ('Y').
 
+=item unused_credit - 'Y' or ''. For suspension reasons only (for now).
+If enabled, the customer will be credited for their remaining time on 
+suspension.
+
 =back
 
 =head1 METHODS
@@ -109,7 +113,6 @@ sub check {
     || $self->ut_number('reason_type')
     || $self->ut_foreign_key('reason_type', 'reason_type', 'typenum')
     || $self->ut_text('reason')
-    || $self->ut_flag('disabled')
   ;
   return $error if $error;
 
@@ -117,11 +120,13 @@ sub check {
     $error = $self->ut_numbern('unsuspend_pkgpart')
           || $self->ut_foreign_keyn('unsuspend_pkgpart', 'part_pkg', 'pkgpart')
           || $self->ut_flag('unsuspend_hold')
+          || $self->ut_flag('unused_credit')
     ;
     return $error if $error;
   } else {
-    $self->set('unsuspend_pkgpart' => '');
-    $self->set('unsuspend_hold'    => '');
+    foreach (qw(unsuspend_pkgpart unsuspend_hold unused_credit)) {
+      $self->set($_ => '');
+    }
   }
 
   $self->SUPER::check;
@@ -178,8 +183,6 @@ sub new_or_existing {
 
 =head1 BUGS
 
-Here by termintes.  Don't use on wooden computers.
-
 =head1 SEE ALSO
 
 L<FS::Record>, schema.html from the base documentation.
index 4011bb0..c0c93dd 100755 (executable)
@@ -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 (file)
index 0000000..f705c68
--- /dev/null
@@ -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;
+
index 4ca8d82..bd35cba 100644 (file)
@@ -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.
index 79a7dc5..105447c 100644 (file)
@@ -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 (file)
index 0000000..753a156
--- /dev/null
@@ -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 (file)
index 0000000..6f51271
--- /dev/null
@@ -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 (file)
index 0000000..dbb6e0a
--- /dev/null
@@ -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 (file)
index 0000000..7fefcc0
--- /dev/null
@@ -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/bin/cdr-thinktel.import b/bin/cdr-thinktel.import
new file mode 100755 (executable)
index 0000000..9afd34c
--- /dev/null
@@ -0,0 +1,127 @@
+#!/usr/bin/perl
+
+use strict;
+use Getopt::Std;
+use Date::Format;
+use File::Temp 'tempdir';
+use Net::FTP;
+use FS::UID qw(adminsuidsetup datasrc dbh);
+use FS::cdr;
+use FS::cdr_batch;
+use FS::Record qw(qsearch qsearchs);
+use Date::Format 'time2str';
+use Date::Parse 'str2time';
+
+
+###
+# parse command line
+###
+
+use vars qw( $opt_d $opt_v $opt_c $opt_s $opt_e $opt_a );
+getopts('dvc:s:e:a');
+
+my ($user, $login, $password) = @ARGV;
+($user and $login and $password) or die &usage;
+
+my $dbh = adminsuidsetup $user;
+$FS::UID::AutoCommit = 0;
+
+# index already-downloaded batches
+my @previous = qsearch({
+    'table'     => 'cdr_batch',
+    'hashref'   => { 'cdrbatch' => {op=>'like', value=>'thinktel%'} },
+    'order_by'  => 'ORDER BY cdrbatch DESC',
+});
+my %exists = map {$_->cdrbatch => 1} @previous;
+
+my $tempdir = tempdir( CLEANUP => !$opt_v );
+
+my $format = 'thinktel';
+my $hostname = 'ucontrol.thinktel.ca';
+
+my $ftp = Net::FTP->new($hostname, Debug => $opt_d)
+  or die "Can't connect to $hostname: $@\n";
+
+$ftp->login($login, $password)
+  or die "Login failed: ".$ftp->message."\n";
+
+###
+# get the file list
+###
+
+warn "Retrieving directory listing\n" if $opt_v;
+
+$ftp->cwd('/');
+my @files = grep { $_ =~ /MetaSwitch/ } $ftp->ls();
+
+warn scalar(@files)." CDR files found.\n" if $opt_v;
+# apply date range
+if ( $opt_a ) {
+  my $most_recent = $previous[0];
+  if ($most_recent) {
+    if ($most_recent->cdrbatch =~ /^thinktel-(\d+)/) {
+      my $date = $1;
+      warn "limiting to dates > $date (from most recent batch)\n" if $opt_v;
+      @files = grep { /^(\d+)_/ && $1 > $date } @files;
+    }
+  } # else download them all
+}
+if ( $opt_s ) {
+  # start date
+  # normalize date format
+  $opt_s = time2str('%Y%m%d', str2time($opt_s)) if $opt_s =~ /\D/;
+  warn "limiting to dates > $opt_s\n" if $opt_v;
+  @files= grep { /^(\d+)_/ && $1 >= $opt_s } @files;
+}
+if ( $opt_e ) {
+  # end date
+  $opt_e = time2str('%Y%m%d', str2time($opt_e)) if $opt_e =~ /\D/;
+  warn "limiting to dates < $opt_e\n" if $opt_v;
+  @files= grep { /^(\d+)_/ && $1 < $opt_e } @files;
+}
+warn scalar(@files) ." files to be downloaded to '$tempdir'\n" if $opt_v;
+foreach my $file (@files) {
+
+  warn "downloading $file\n" if $opt_v;
+  $ftp->get($file, "$tempdir/$file");
+  warn "processing $file\n" if $opt_v;
+
+  my $batchname = "$format-$file";
+  if ($exists{$batchname}) {
+    warn "already imported $file\n";
+    next;
+  }
+  my $import_options = {
+    'file'            => "$tempdir/$file",
+    'format'          => $format,
+    'batch_namevalue' => $batchname,
+    'empty_ok'        => 1,
+  };
+  $import_options->{'cdrtypenum'} = $opt_c if $opt_c;
+  
+  my $error = FS::cdr::batch_import($import_options);
+
+  if ( $error ) {
+    die "error processing $file: $error\n";
+  }
+}
+warn "finished\n" if $opt_v;
+$dbh->commit;
+
+###
+# subs
+###
+
+sub usage {
+  "Usage: \n  cdr-thinktel.import [ options ] user login password
+  Options:
+    -v: be verbose
+    -d: enable FTP debugging (very noisy)
+    -c num: apply a cdrtypenum to the imported CDRs
+    -s date: start date
+    -e date: end date
+    -a: automatically choose start date from most recently downloaded batch
+
+";
+}
+
index 033d892..07d3914 100755 (executable)
@@ -195,6 +195,7 @@ install-stamp: build-stamp
        perl -p -i -e "\
           s'${TMP}?''g;\
         " ${TMP}-lib/usr/bin/* \
+         ${TMP}-webui/usr/local/etc/freeside/handler.pl
 
        #RT Config
 
index 5fcf77f..f16de77 100755 (executable)
@@ -1,5 +1,11 @@
 <%= include('header', 'Purchase additional package') %>
 
-<%= include('order_pkg') %>
+<%= if (grep $_ eq 'Purchase additional package', @menu_disable){
+        $OUT .= "This functionality has been disabled";
+} else { 
+
+include('order_pkg') 
+
+} %>
 
 <%= include('footer') %>
index 35728e7..5a01043 100644 (file)
@@ -11,7 +11,7 @@
 
 <%= if (!$error) {
   $selfurl =~ s/\?.*//;
-  $OUT .= "Your password has been changed.  You can now <A HREF=\"$selfurl\">log in</A>.";
+  $OUT .= "Your password has been changed.  You can now <A HREF=\"$selfurl?agentnum=$agentnum\">log in</A>.";
   }
 %>
 
diff --git a/httemplate/browse/circuit_provider.html b/httemplate/browse/circuit_provider.html
new file mode 100644 (file)
index 0000000..12f6532
--- /dev/null
@@ -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 (file)
index 0000000..830ccf7
--- /dev/null
@@ -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 (file)
index 0000000..a145d54
--- /dev/null
@@ -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 (file)
index 0000000..cfa27e8
--- /dev/null
@@ -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>
index e3fd97c..bdfb99a 100755 (executable)
@@ -180,9 +180,11 @@ if ( $cgi->param('redirect') ) {
 my $html_init = 
   include('/elements/init_overlib.html') .
   include('/elements/input-fcc_options.html', js_only => 1) .
-  include('.style');
+  include('.style') .
+  include('.script');
 
-my $html_form = qq!<FORM ACTION="${p}edit/process/bulk-part_pkg-fcc.html" METHOD="POST">
+my $html_form = qq!<FORM ACTION="${p}edit/process/bulk-part_pkg-fcc.html" METHOD="POST" NAME="bulk-part_pkg-fcc">
+  <INPUT TYPE="hidden" NAME="jump">
   ( show class: !.
   include('/elements/select-pkg_class.html',
             #'curr_value'    => $classnum,
@@ -193,20 +195,13 @@ my $html_form = qq!<FORM ACTION="${p}edit/process/bulk-part_pkg-fcc.html" METHOD
             'disable_empty' => 1,
          ).
   ' )
-  <BR><BR>' .
-  qq!<SCRIPT TYPE="text/javascript">
-  function filter_change() {
-    window.location = '! . $cgi->self_url . qq!?classnum='
-      + document.getElementById('classnum').value;
-  }
-  </SCRIPT>!;
+  <BR><BR>';
+
 
 # restore this only after creating $html_form
 $cgi->param('classnum', $classnum) if length($classnum);
 
-my $html_foot = qq!
-  <INPUT TYPE="submit" VALUE="Save changes">
-  </FORM>!;
+my $html_foot = '</FORM>';
 
 my @menubar =
   ( 'Package definitions' => $p.'browse/part_pkg.cgi' );
@@ -224,3 +219,17 @@ my @menubar =
   }
 </style>
 </%def>
+<%def .script>
+<script type="text/javascript">
+  function finish_edit_fcc(id) {
+    cClick();
+    document.forms['bulk-part_pkg-fcc']['jump'].value = id;
+    document.forms['bulk-part_pkg-fcc'].submit(); //immediately save/refresh
+  }
+
+  function filter_change() {
+    window.location = '! . $cgi->self_url . qq!?classnum='
+      + document.getElementById('classnum').value;
+  }
+</script>
+</%def>
index 8d3711d..5e8d9e5 100644 (file)
@@ -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 (file)
index 0000000..6c8dced
--- /dev/null
@@ -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 (file)
index 0000000..0317bce
--- /dev/null
@@ -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 (file)
index 0000000..8977588
--- /dev/null
@@ -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>
index 40faddc..85f7943 100644 (file)
@@ -80,9 +80,9 @@
 <& /elements/tr-select-reason.html,
               'field'          => 'reasonnum',
               'reason_class'   => 'R',
-              #XXX reconcile both this and show_taxes wanteding to enable this
+              #XXX reconcile both this and show_taxes wanting to enable this
               'id'             => 'select_reason',
-              'control_button' => "document.getElementById('credit_button')",
+              'control_button' => 'credit_button',
               'cgi'            => $cgi,
 &>
 
index a3565f1..29801ef 100755 (executable)
@@ -24,7 +24,7 @@
 <& /elements/tr-select-reason.html,
               'field'          => 'reasonnum',
               'reason_class'   => 'R',
-              'control_button' => "document.getElementById('confirm_credit_button')",
+              'control_button' => 'confirm_credit_button',
               'cgi'            => $cgi,
 &>
 
index 5a8835f..6a9deb9 100644 (file)
@@ -100,14 +100,13 @@ function copyelement(from, to) {
   //alert(from + " (" + from.type + "): " + to.name + " => " + to.value);
 }
 
-% # the value in pre+'censustract' is the confirmed censustract; if it's set,
-% # and the user hasn't changed it manually, skip this
+% # the value in pre+'censustract' is the confirmed censustract (either from
+% # the previous saved record, or from address standardization (if the backend
+% # supports it), or from an aborted previous submit. only need to reconfirm
+% # if it's empty.
 function confirm_censustract(pre) {
   var cf = document.CustomerForm;
-  if ( cf.elements[pre+'censustract'].value == '' ||
-         cf.elements[pre+'enter_censustract'].value != 
-         cf.elements[pre+'censustract'].value )
-  {
+  if ( cf.elements[pre+'censustract'].value == '' ) {
     var address_info = form_address_info();
     address_info[pre+'latitude']  = cf.elements[pre+'latitude'].value;
     address_info[pre+'longitude'] = cf.elements[pre+'longitude'].value;
@@ -116,10 +115,13 @@ function confirm_censustract(pre) {
         '<%$p%>/misc/confirm-censustract.html',
         'q=' + encodeURIComponent(JSON.stringify(address_info)),
         function() {
-          overlib( OLresponseAJAX, CAPTION, 'Confirm censustract', STICKY,
-            AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, DRAGGABLE, WIDTH,
-            576, HEIGHT, 268, BGCOLOR, '#333399', CGCOLOR, '#333399',
-            TEXTSIZE, 3 );
+          if ( OLresponseAJAX ) {
+            overlib( OLresponseAJAX, CAPTION, 'Confirm censustract', STICKY,
+              AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, DRAGGABLE, WIDTH,
+              576, HEIGHT, 268, BGCOLOR, '#333399', CGCOLOR, '#333399',
+              TEXTSIZE, 3 );
+          } else
+            submit_continue();
         },
         0);
   } else submit_continue();
index 64901a8..6dcb602 100644 (file)
@@ -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">
index fc29327..97b630f 100644 (file)
                    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';
                                            };
 
                    } 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,
index af69f71..2ae9df3 100755 (executable)
@@ -895,6 +895,11 @@ my $javascript = <<'END';
       }
     }
 
+    function finish_edit_fcc(id) {
+      cClick();
+      show_fcc_options(id); // refresh the display
+    }
+
 END
 
 my $warning =
index 4a0fb2a..8ef3308 100644 (file)
@@ -17,7 +17,7 @@
 %   }
 <% $cgi->redirect($fsurl.'browse/part_pkg-fcc.html?redirect='.$session) %>
 % } else {
-<% $cgi->redirect($fsurl.'browse/part_pkg-fcc.html?classnum='.$classnum) %>
+<% $cgi->redirect($fsurl.'browse/part_pkg-fcc.html?classnum='.$classnum.$jump) %>
 % }
 <%init>
 my $curuser = $FS::CurrentUser::CurrentUser;
@@ -40,4 +40,8 @@ foreach my $param ($cgi->param) {
 
 my $classnum = $cgi->param('classnum');
 
+my $jump = '';
+if ( $cgi->param('jump') =~ /^pkgpart(\d+)$/ ) {
+  $jump = '#'.$1;
+}
 </%init>
diff --git a/httemplate/edit/process/circuit_provider.html b/httemplate/edit/process/circuit_provider.html
new file mode 100644 (file)
index 0000000..0a91a17
--- /dev/null
@@ -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 (file)
index 0000000..94d29c0
--- /dev/null
@@ -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 (file)
index 0000000..58f461e
--- /dev/null
@@ -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>
index 8e66368..75900bd 100644 (file)
@@ -27,19 +27,35 @@ foreach my $billpkgnum_setuprecur (@billpkgnum_setuprecurs) {
   push @amounts,     $amount;
 }
 
-my $error = FS::cust_credit->credit_lineitems(
+my $reasonnum = $cgi->param('reasonnum');
+$reasonnum =~ /^(-?\d+)$/ or die "Illegal reasonnum";
+$reasonnum = $1;
+
+my $error;
+if ($reasonnum == -1) {
+  my $new_reason = FS::reason->new({
+    map { $_ => scalar( $cgi->param("select_reason_new_$_") ) }
+    qw( reason_type reason )
+  });
+  $error = $new_reason->insert;
+  $reasonnum = $new_reason->reasonnum;
+}
+
+if ( !$reasonnum ) {
+  $error ||= 'Reason required'
+}
+
+$error ||= FS::cust_credit->credit_lineitems(
   #the lineitems to credit
   'billpkgnums'       => \@billpkgnums,
   'setuprecurs'       => \@setuprecurs,
   'amounts'           => \@amounts,
   'apply'             => ( $cgi->param('apply') eq 'yes' ),
+  'reasonnum'         => $reasonnum,
 
-  #the credit
-  'newreasonnum'      => scalar($cgi->param('newreasonnum')),
-  'newreasonnum_type' => scalar($cgi->param('newreasonnumT')),
   map { $_ => scalar($cgi->param($_)) }
     #fields('cust_credit')  
-    qw( custnum _date amount reason reasonnum addlinfo ), #pkgnum eventnum
+    qw( custnum _date amount addlinfo ), #pkgnum eventnum
 );
 
 </%init>
index 245f31a..e442d7f 100755 (executable)
@@ -1,5 +1,4 @@
 %if ( $error ) {
-%  $cgi->param('reasonnum', $reasonnum);
 %  $cgi->param('error', $error);
 %  $dbh->rollback if $oldAutoCommit;
 %  
@@ -37,19 +36,11 @@ my $oldAutoCommit = $FS::UID::AutoCommit;
 local $FS::UID::AutoCommit = 0;
 my $dbh = dbh;
 
-my $error = '';
-if ($reasonnum == -1) {
-
-  $error = 'Enter a new reason (or select an existing one)'
-    unless $cgi->param('newreasonnum') !~ /^\s*$/;
-  my $reason = new FS::reason {
-                 'reason_type' => scalar($cgi->param('newreasonnumT')),
-                 'reason'      => scalar($cgi->param('newreasonnum')),
-               };
-  $error ||= $reason->insert;
-  $cgi->param('reasonnum', $reason->reasonnum)
-    unless $error;
+my ($reasonnum, $error) = $m->comp('/misc/process/elements/reason');
+if (!$reasonnum) {
+  $error ||= 'Reason required'
 }
+$cgi->param('reasonnum', $reasonnum) unless $error;
 
 unless ($error) {
   my $new = new FS::cust_credit ( {
index 55ecc5f..ca336a1 100644 (file)
@@ -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);
     }
index 481439d..bac6924 100644 (file)
             my $value = join(',', $cgi->param( "$prefix$option" ) );
 
             if ( $option eq 'reasonnum' && $value == -1 ) {
-              $value = {
-                'typenum' => scalar( $cgi->param( "new$prefix${option}T" ) ),
-                'reason'  => scalar( $cgi->param( "new$prefix${option}"  ) ),
-              };
+              my $reason_prefix = $object->action . '_' . $option .  '_new_';
+              my $new_reason = FS::reason->new;
+              foreach ( qw( reason_type reason unsuspend pkgpart 
+                            unsuspend_hold unused_credit ) ) {
+                $new_reason->set($_, $cgi->param("$reason_prefix$_"));
+              }
+              warn Dumper $new_reason;
+              my $error = $new_reason->insert;
+              die "error creating reason: $error" if $error;
+              $value = $new_reason->reasonnum;
             }
-
             ( $option => $value );
           }
           @{ $object->option_fields_listref };
diff --git a/httemplate/edit/process/svc_circuit.html b/httemplate/edit/process/svc_circuit.html
new file mode 100644 (file)
index 0000000..d28f913
--- /dev/null
@@ -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 (file)
index 0000000..3f9bad5
--- /dev/null
@@ -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>
index f858205..f9c0d40 100644 (file)
@@ -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>
index fb2500f..064c647 100644 (file)
@@ -80,7 +80,8 @@ function show_fcc_options(id) {
     }
   } // is_phone
   if ( curr_values['is_voip'] ) {
-    out += '<li><strong>VoIP</strong> telephone service</li>';
+    out += '<li><strong>VoIP</strong> telephone service over <strong>' 
+            + media + '</strong></li>';
     out += '<li><strong>' + curr_values['voip_sessions'] + 
            '</strong> sessions allowed</li>';
     if ( curr_values['voip_lastmile'] ) {
index 5cdc424..214a7d5 100644 (file)
@@ -59,12 +59,7 @@ Example:
     </TR>
 
 % } else {
-
-    <INPUT TYPE     = "hidden"
-           NAME     = "<%$pre%>locationname"
-           ID       = "<%$pre%>locationname"
-           VALUE    = "<% $object->get('locationname') |h %>"
-    >
+    <& hidden.html, field => $pre.'locationname', value => $object->get('locationname') &>
 
 % }
 
@@ -102,10 +97,7 @@ Example:
 
 % } else { # alternate format
 
-      <INPUT TYPE  = "hidden"
-             NAME  = "<%$pre%>address2"
-             VALUE = "<% $object->get('address2') |h %>"
-      >
+<& hidden.html, field => $pre.'address2', value => $object->get('address2') &>
 
 <TR>
     <<%$th%> ALIGN="right">Unit&nbsp;type&nbsp;and&nbsp;#</<%$th%>>
@@ -227,14 +219,14 @@ Example:
 </TR>
 % } else {
 %   foreach (qw(latitude longitude)) {
-<INPUT TYPE="hidden" NAME="<% $_ %>" ID="<% $_ %>" VALUE="<% $object->get($_) |h%>">
+<& hidden.html, field => $pre.$_, value => $object->get($_) &>
 %   }
 % }
-<INPUT TYPE="hidden" NAME="<%$pre%>coord_auto" VALUE="<% $object->coord_auto %>">
-
-<INPUT TYPE="hidden" NAME="<%$pre%>geocode" VALUE="<% $object->geocode %>">
-<INPUT TYPE="hidden" NAME="<%$pre%>censustract" VALUE="<% $object->censustract %>">
-<INPUT TYPE="hidden" NAME="<%$pre%>censusyear" VALUE="<% $object->censusyear %>">
+%
+% foreach (qw(coord_auto geocode censustract censusyear)) {
+  <& hidden.html, field => $pre.$_, value => $object->get($_) &>
+% }
+%
 % if ( $opt{enable_censustract} ) {
 <TR>
   <TD ALIGN="right">Census&nbsp;tract</TD>
@@ -259,7 +251,7 @@ Example:
     </TD>
   </TR>
 % } else {
-    <INPUT TYPE="hidden" ID="<%$pre%>" NAME="<%$pre%>district" VALUE="<% $object->district %>">
+  <& hidden.html, field => $pre.'district', value => $object->get('district') &>
 % }
 
 %# For address standardization:
@@ -267,11 +259,11 @@ Example:
 %# to re-standardize
 % foreach (qw(address1 city state country zip latitude
 %             longitude censustract district addr_clean) ) {
-<INPUT TYPE="hidden" NAME="old_<%$pre.$_%>" ID="old_<%$pre.$_%>" VALUE="<% $object->get($_) |h%>">
+<& hidden.html, field => 'old_'.$pre.$_, value => $object->get($_) &>
 % }
 %# Placeholders
-<INPUT TYPE="hidden" NAME="<%$pre%>cachenum" VALUE="">
-<INPUT TYPE="hidden" NAME="<%$pre%>addr_clean" VALUE="">
+<& hidden.html, field => $pre.'cachenum', value => '' &>
+<& hidden.html, field => $pre.'addr_clean', value => '' &>
 
 <SCRIPT TYPE="text/javascript">
 <&| /elements/onload.js &>
@@ -306,6 +298,26 @@ Example:
       el.attachEvent('onchange', clear_coords);
     }
   }
+  function clear_censustract() {
+    // if the user manually edits the census tract, clear the 'hard' census
+    // tract field so that we can re-verify and present a confirmation popup 
+
+    // get the ID of the hidden censustract field
+    var censustract_id = this.id.replace('enter_', '');
+    var el = document.getElementById(censustract_id);
+    if (el) {
+      el.value = '';
+    }
+  }
+  var el = document.getElementById('<%$pre%>enter_censustract');
+  if (el) {
+    if ( el.addEventListener ) {
+      el.addEventListener('change', clear_censustract);
+    } else if ( el.attachEvent ) {
+      el.attachEvent('onchange', clear_censustract);
+    }
+  }
+
 </&>
 </SCRIPT>
 
index 3236bc1..03ce201 100644 (file)
@@ -564,6 +564,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' ];
@@ -584,6 +590,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');
 
index 817a2e3..ff7183b 100644 (file)
@@ -279,10 +279,7 @@ function setselect(el, value) {
 function confirm_censustract() {
 %   if ( FS::Conf->new->exists('cust_main-require_censustract') ) {
   var form = document.<% $formname %>;
-  // this is the existing/confirmed censustract, not the manually entered one
-  if ( form.elements['censustract'].value == '' ||
-       form.elements['censustract'].value != 
-          form.elements['enter_censustract'].value ) {
+  if ( form.elements['censustract'].value == '' ) {
     var address_info = form_address_info();
     address_info['latitude']  = form.elements['latitude'].value;
     address_info['longitude'] = form.elements['longitude'].value;
@@ -290,10 +287,15 @@ function confirm_censustract() {
         '<%$p%>/misc/confirm-censustract.html',
         'q=' + encodeURIComponent(JSON.stringify(address_info)),
         function() {
-          overlib( OLresponseAJAX, CAPTION, 'Confirm censustract', STICKY,
-            AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, DRAGGABLE, WIDTH,
-            576, HEIGHT, 268, BGCOLOR, '#333399', CGCOLOR, '#333399',
-            TEXTSIZE, 3 );
+          if ( OLresponseAJAX ) {
+            overlib( OLresponseAJAX, CAPTION, 'Confirm censustract', STICKY,
+              AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, DRAGGABLE, WIDTH,
+              576, HEIGHT, 268, BGCOLOR, '#333399', CGCOLOR, '#333399',
+              TEXTSIZE, 3 );
+          } else {
+            // no response
+            <% $post_censustract %>;
+          }
         },
         0);
   } else {
index bd014f1..7a4b349 100644 (file)
@@ -1,4 +1,4 @@
-% if ($censustract) {
+% if ($location->country eq 'US' and $censustract) {
 <TR>
   <TD ALIGN="right"><% mt('Census tract') |h %></TD>
   <TD COLSPAN=5>
index 87117ef..1f63588 100644 (file)
@@ -7,96 +7,9 @@
 </STYLE>
 <TR>
   <TH COLSPAN=2>
-    <& hidden.html, 'id' => $id, @_ &>
-%#    <& input-text.html, 'id' => $id, @_ &> # XXX debugging
-    <UL ID="<%$id%>_display_fcc_options" CLASS="fcc_options">
-    </UL>
-    <BUTTON TYPE="button" onclick="edit_fcc_options()">
-      Edit
-    </BUTTON>
-% # show some kind of useful summary of the FCC options here
+    <& input-fcc_options.html, 'id' => $id, @_ &>
   </TH>
 </TR>
-<SCRIPT TYPE="text/javascript">
-function edit_fcc_options() {
-  <& popup_link_onclick.html,
-  'action'      => $fsurl.'misc/part_pkg_fcc_options.html?id=' . $id,
-  'actionlabel' => 'FCC Form 477 options',
-  'width'       => 760,
-  'height'      => 600,
-  &>
-}
-var technology_labels = <% encode_json(FS::part_pkg_fcc_option->technology_labels) %>;
-function show_fcc_options() {
-  var curr_values = JSON.parse(document.getElementById('<% $id %>').value);
-  // hardcoded for the same reasons as misc/part_pkg_fcc_options
-  var out = '';
-  var tech = curr_values['technology'];
-  if ( tech ) {
-    if (technology_labels[tech]) {
-      tech = technology_labels[tech];
-    } else {
-      tech = 'Technology '+tech; // unknown?
-    }
-  }
-  var media = curr_values['media'] || 'unknown media';
-  media = media.toLowerCase();
-  if ( curr_values['is_consumer'] ) {
-    out += '<li><strong>Consumer-grade</strong></li>>';
-  } else {
-    out += '<li><strong>Business-grade</strong></li>';
-  }
-  if ( curr_values['is_broadband'] ) {
-    out += '<li>Broadband via <strong>' + tech + '</strong>'
-        +  '<li><strong>' + curr_values['broadband_downstream']
-        +  'Mbps </strong> down / '
-        +  '<strong>' + curr_values['broadband_upstream']
-        +  'Mbps </strong> up</li>';
-  }
-  if ( curr_values['is_phone'] ) {
-    if ( curr_values['phone_wholesale'] ) {
-      out += '<li>Wholesale telephone</li>';
-      if ( curr_values['phone_vges'] ) {
-        out += '<li><strong>' + curr_values['phone_vges'] + '</strong>'
-            +  ' switched voice-grade lines</li>';
-      }
-      if ( curr_values['phone_circuits'] ) {
-        out += '<li><strong>' + curr_values['phone_circuits'] + '</strong>'
-            +  ' unswitched circuits</li>';
-      }
-    } else {
-      // enduser service
-      out += '<li>Local telephone over <strong>' + media + '</strong></li>'
-          +  '<li><strong>' + curr_values['phone_lines']
-          +  '</strong> voice-grade lines</li>';
-      if ( curr_values['phone_localloop'] == 'resale' ) {
-        out += '<li><strong>Resold</strong> from another carrier</li>>';
-      } else if ( curr_values['phone_localloop'] == 'leased' ) {
-        out += '<li>Using <strong>leased circuits</strong> from another carrier</li>';
-      } else if ( curr_values['phone_localloop'] == 'owned' ) {
-        out += '<li>Using <strong>our own circuits</strong></li>';
-      }
-      if ( curr_values['phone_longdistance'] ) {
-        out += '<li>Includes <strong>long-distance service</strong></li>';
-      }
-    }
-  } // is_phone
-  if ( curr_values['is_voip'] ) {
-    out += '<li><strong>VoIP</strong> telephone service</li>';
-    if ( curr_values['voip_ott'] ) {
-      out += '<li>Using a <strong>separate</strong> last-mile connection</li>';
-    } else {
-      out += '<li><strong>Including</strong> last-mile connection</li>';
-    }
-  } // is_voip
-
-  var out_ul = document.getElementById('<% $id %>_display_fcc_options');
-  out_ul.innerHTML = out;
-}
-<&| onload.js &>
-  show_fcc_options();
-</&>
-</SCRIPT>
 <%init>
 my %opt = @_;
 my $id = $opt{id} || $opt{field};
index b7a715b..0df7c05 100755 (executable)
@@ -29,109 +29,119 @@ Example:
 
 </%doc>
 
+% # note style improvements.
+% # - no more conditionally included code here
+% # - callers are not expected to pass javascript fragments
+% # - no redundant checking of ACLs or parameters
+% # - form fields are grouped for easy management
+% # - use the standard select-table widget instead of ad hoc crap
 <SCRIPT TYPE="text/javascript">
-  function sh_add<% $func_suffix %>()
-  {
-    var hints = <% encode_json(\@hints) %>;
+  function <% $id %>_changed() {
+    var hints = <% encode_json(\%all_hints) %>;
     var select_reason = document.getElementById('<% $id %>');
 
-% if ( $class eq 'S' ) {    
     document.getElementById('<% $id %>_hint').innerHTML =
-      hints[select_reason.selectedIndex];
-% }
+      hints[select_reason.value] || '';
 
-    if (select_reason.selectedIndex == 0){
-      <% $controlledbutton ? $controlledbutton.'.disabled = true;' : ';' %>
-    }else{
-      <% $controlledbutton ? $controlledbutton.'.disabled = false;' : ';' %>
+    // toggle submit button state
+    var submit_button = document.getElementById(<% $opt{control_button} |js_string %>);
+    if (submit_button) {
+      submit_button.disabled = ( select_reason.value == 0 );
     }
 
-%if ($curuser->access_right($add_access_right)){
-
-    if (select_reason.selectedIndex == 
-         (select_reason.length - 1)) {
-      document.getElementById('new<% $id %>').disabled = false;
-      document.getElementById('new<% $id %>').style.display = 'inline';
-      document.getElementById('new<% $id %>Label').style.display = 'inline';
-      document.getElementById('new<% $id %>T').disabled = false;
-      document.getElementById('new<% $id %>T').style.display = 'inline';
-      document.getElementById('new<% $id %>TLabel').style.display = 'inline';
+    // toggle visibility of 'new reason' fields
+    var new_fields = document.getElementById('<% $id %>_new_fields');
+    if ( select_reason.value == -1 ) {
+      new_fields.disabled = false;
+      new_fields.style.display = '';
     } else {
-      document.getElementById('new<% $id %>').disabled = true;
-      document.getElementById('new<% $id %>').style.display = 'none';
-      document.getElementById('new<% $id %>Label').style.display = 'none';
-      document.getElementById('new<% $id %>T').disabled = true;
-      document.getElementById('new<% $id %>T').style.display = 'none';
-      document.getElementById('new<% $id %>TLabel').style.display = 'none';
+      new_fields.disabled = true;
+      new_fields.style.display = 'none';
     }
 
-%}
-
   }
+  <&| onload.js &> <% $id %>_changed(); </&>
 </SCRIPT>
 
-<TR>
-  <TD ALIGN="right"><% mt('Reason') |h %></TD>
-  <TD>
-    <SELECT id="<% $id %>" name="<% $name %>" onFocus="sh_add<% $func_suffix %>()" onChange="sh_add<% $func_suffix %>()">
-      <OPTION VALUE="" <% ($init_reason eq '') ? 'SELECTED' : '' %>><% mt('Select Reason...') |h %></OPTION>
-%    foreach my $reason (@reasons) {
-      <OPTION VALUE="<% $reason->reasonnum %>" <% ($init_reason == $reason->reasonnum) ? 'SELECTED' : '' %>><% $reason->reasontype->type %> : <% $reason->reason %></OPTION>
-%    }
-%    if ($curuser->access_right($add_access_right)) {
-      <OPTION VALUE="-1" <% ($init_reason == -1) ? 'SELECTED' : '' %>><% mt('Add new reason') |h %></OPTION>
-%    }
-%
-    </SELECT>
-  </TD>
-</TR>
-
-%   my @types = qsearch( 'reason_type', { 'class' => $class } );
-%   if (scalar(@types) < 1) {  # we should never reach this
-<TR>
-  <TD ALIGN="right">
-    <P><% mt('No reason types. Please add some.') |h %></P>
-  </TD>
-</TR>
-%   }elsif (scalar(@types) == 1) {
-<TR>
-  <TD ALIGN="right">
-    <P id="new<% $name %>TLabel" style="display:<% $display %>"><% mt('Reason Type') |h %></P>
-  </TD>
-  <TD>
-    <P id="new<% $name %>T" disabled="<% $disabled %>" style="display:<% $display %>"><% $types[0]->type %>
-    <INPUT type="hidden" name="new<% $name %>T" value="<% $types[0]->typenum %>">
-  </TD>
-</TR>
-
-%   }else{
-
-<TR>
-  <TD ALIGN="right">
-    <P id="new<% $id %>TLabel" style="display:<% $display %>"><% mt('Reason Type') |h %></P>
-  </TD>
-  <TD>
-    <SELECT id="new<% $id %>T" name="new<% $name %>T" "<% $disabled %>" style="display:<% $display %>">
-%     for my $type (@types) {
-        <OPTION VALUE="<% $type->typenum %>" <% ($init_type == $type->typenum) ? 'SELECTED' : '' %>><% $type->type %></OPTION>
-%     }
-    </SELECT>
-  </TD>
-</TR>
-%   }
+%# sadly can't just use add_inline here, as we have non-text fields
+<& tr-select-table.html,
+  'label'           => 'Reason',
+  'field'           => $name,
+  'id'              => $id,
+  'table'           => 'reason',
+  'records'         => \@reasons,
+  'label_callback'  => sub { my $reason = shift;
+                             $reason->type . ' : ' .  $reason->reason },
+  'disable_empty'   => 1,
+  'pre_options'     => [ 0 => 'Select reason...' ],
+  'post_options'    => [ -1 => 'Add new reason' ],
+  'curr_value'      => $init_reason,
+  'onchange'        => $id.'_changed()',
+&>
+
+% # "add new reason" fields
+% # should be a <fieldset>, but that doesn't fit well into the table
+
+<TR id="<% $id %>_new_fields">
+  <TD COLSPAN=2>
+    <TABLE CLASS="inv" STYLE="text-align: left">
+
+      <& tr-input-text.html,
+        label => 'New reason',
+        field => $id.'_new_reason'
+      &>
+
+% my @types = qsearch( 'reason_type', { 'class' => $class } );
+% if (scalar(@types) < 1) {  # we should never reach this
+      <TR>
+        <TD ALIGN="right">
+          <P><% mt('No reason types. Please add some.') |h %></P>
+        </TD>
+      </TR>
+% } elsif (scalar(@types) == 1) {
+      <& tr-fixed.html,
+        label => 'Reason type',
+        field => $id.'_new_reason_type',
+        curr_value => $types[0]->typenum,
+        formatted_value => $types[0]->type,
+      &>
+% } else { # more than one type, the normal case
+      <& tr-select-table.html,
+        label         => 'Reason type',
+        field         => $id.'_new_reason_type',
+        table         => 'reason_type',
+        name_col      => 'type',
+        hashref       => { 'class' => $class },
+        disable_empty => 1,
+      &>
+% } # scalar(@types)
 
 % if ( $class eq 'S' ) {
-<TR>
-  <TD COLSPAN=2 ALIGN="center" id="<% $id %>_hint">
-  </TD>
-</TR>
+      <& tr-checkbox.html,
+        label => 'Credit the unused portion of service when suspending',
+        field => $id.'_new_unused_credit',
+        value => 'Y'
+      &>
+      <& tr-select-part_pkg.html,
+        label   => 'Charge this fee when unsuspending',
+        field   => $id.'_new_unsuspend_pkgpart',
+        hashref => { disabled => '', freq => '0' },
+        empty_label => 'none',
+      &>
+      <& tr-checkbox.html,
+        label => 'Hold unsuspension fee until the next bill',
+        field => $id.'_new_unsuspend_hold',
+        value => 'Y',
+      &>
 % }
+    </table>
+  </td>
+</tr>
 
+% # container for hints
 <TR>
-  <TD ALIGN="right">
-    <P id="new<% $id %>Label" style="display:<% $display %>"><% mt('New Reason') |h %></P>
+  <TD COLSPAN=2 ALIGN="center" id="<% $id %>_hint" style="font-size:small">
   </TD>
-  <TD><INPUT id="new<% $id %>" name="new<% $name %>" type="text" value="<% $init_newreason |h %>" "<% $disabled %>" style="display:<% $display %>"></TD>
 </TR>
 
 <%init>
@@ -148,11 +158,8 @@ if ( $opt{'cgi'} ) {
   $init_reason = $opt{'curr_value'};
 }
 
-my $controlledbutton = $opt{'control_button'};
-
-( my $func_suffix = $name ) =~ s/\./_/g;
-
-my $id = $opt{'id'} || $func_suffix;
+my $id = $opt{'id'} || $name;
+$id =~ s/\./_/g; # for edit/part_event
 
 my $add_access_right;
 if ($class eq 'C') {
@@ -167,41 +174,21 @@ if ($class eq 'C') {
   die "illegal class: $class";
 }
 
-my( $display, $disabled ) = ( 'none', 'DISABLED' );
-my( $init_type, $init_newreason ) = ( '', '' );
-if ($init_reason == -1 || ref($init_reason) ) {
-
-  $display = 'inline';
-  $disabled = '';
-
-  if ( ref($init_reason) ) {
-    $init_type      = $init_reason->{'typenum'};
-    $init_newreason = $init_reason->{'reason'};
-    $init_reason = -1;
-  } elsif ( $opt{'cgi'} ) {
-    $init_type      = $opt{'cgi'}->param( "new${name}T" );
-    $init_newreason = $opt{'cgi'}->param( "new$name"    );
-  }
-
-}
-
-my $extra_sql =
-  "WHERE class = '$class' and (disabled = '' OR disabled is NULL)";
-
 my @reasons = qsearch({
-  table     => 'reason', 
-  hashref   => {},
-  extra_sql => $extra_sql,
-  addl_from => 'LEFT JOIN reason_type '.
-               ' ON reason_type.typenum = reason.reason_type',
-  order_by  => 'ORDER BY reason_type.type ASC, reason.reason ASC',
+  'table'           => 'reason',
+  'addl_from'       => ' LEFT JOIN reason_type'.
+                       ' ON (reason.reason_type = reason_type.typenum)',
+  'hashref'         => { disabled => '' },
+  'extra_sql'       => " AND reason_type.class = '$class'",
+  'order_by'        => ' ORDER BY type, reason',
 });
 
-my @hints;
+my %all_hints;
 if ( $class eq 'S' ) {
   my $conf = FS::Conf->new;
-  @hints = ( '' );
+  %all_hints = ( 0 => '', -1 => '' );
   foreach my $reason (@reasons) {
+    my @hints;
     if ( $reason->unsuspend_pkgpart ) {
       my $part_pkg = FS::part_pkg->by_key($reason->unsuspend_pkgpart);
       if ( $part_pkg ) {
@@ -225,15 +212,13 @@ if ( $class eq 'S' ) {
           '<FONT COLOR="#ff0000">Unsuspend pkg #'.$reason->unsuspend_pkgpart.
           ' not found.</FONT>';
       }
-    } else { #no unsuspend_pkgpart
-      push @hints, '';
     }
+    if ( $reason->unused_credit ) {
+      push @hints, mt('The customer will be credited for unused time.');
+    }
+    $all_hints{ $reason->reasonnum } = join('<BR>', @hints);
   }
-  push @hints, ''; # for the "new reason" case
-  @hints = map {'<FONT SIZE="-1">'.$_.'</FONT>'} @hints;
 }
 
-
 my $curuser = $FS::CurrentUser::CurrentUser;
-
 </%init>
diff --git a/httemplate/elements/tr-select-svc_circuit.html b/httemplate/elements/tr-select-svc_circuit.html
new file mode 100644 (file)
index 0000000..fb55501
--- /dev/null
@@ -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>
index ebf1119..8536702 100644 (file)
@@ -48,7 +48,7 @@ toggle(false);
              'field'          => 'reasonnum',
              'reason_class'   => 'C',
              'cgi'            => $cgi,
-             'control_button' => "document.getElementById('confirm_cancel_cust_button')",
+             'control_button' => 'confirm_cancel_cust_button',
 &>
 
 </TABLE>
index e2734e9..c80b2b2 100755 (executable)
@@ -6,9 +6,9 @@
 <INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
 <INPUT TYPE="hidden" NAME="method" VALUE="<% $method %>">
 
-<BR><BR>
+<BR>
 <% emt(ucfirst($method)." [_1]", $part_pkg->pkg_comment(cust_pkg=>$cust_pkg) ) %>
-<% ntable("#cccccc", 2) %>
+<table style="background-color: #cccccc; border-spacing: 2; width: 100%">
 
 % my $date_init = 0;
 % if ($method eq 'expire' || $method eq 'adjourn' || $method eq 'resume') {
@@ -58,7 +58,7 @@
        field          => 'reasonnum',
        reason_class   => $class,
        curr_value     => $reasonnum,
-       control_button => "document.getElementById('confirm_cancel_pkg_button')",
+       control_button => "confirm_cancel_pkg_button",
   &>
 % }
 
index 880cade..024bc17 100644 (file)
@@ -1,3 +1,10 @@
+% if ( !$error and !$new_tract ) {
+%   # then set_censustract returned nothing
+%   # because it's not relevant for this address
+%   # so output nothing (forces confirm_censustract() to continue)
+% $m->clear_buffer;
+% $m->abort;
+% }
 <CENTER><BR><B>
 % if ( $error ) {
 Census tract error
index 2ae9f10..a78a8b3 100755 (executable)
@@ -22,51 +22,43 @@ if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
   $custnum = $1;
 }
 
-#false laziness w/process/cancel_pkg.html
 
-#untaint reasonnum
-my $reasonnum = $cgi->param('reasonnum');
-$reasonnum =~ /^(-?\d+)$/ || die "Illegal reasonnum";
-$reasonnum = $1;
-
-if ($reasonnum == -1) {
-  $reasonnum = {
-    'typenum' => scalar( $cgi->param('newreasonnumT') ),
-    'reason'  => scalar( $cgi->param('newreasonnum' ) ),
-  };
+#untaint reasonnum / create new reason
+my ($reasonnum, $error) = $m->comp('process/elements/reason');
+if (!$reasonnum) {
+  $error ||= 'Reason required'
 }
 
-#eslaf
-
 my $cust_main = qsearchs( {
   'table'     => 'cust_main',
   'hashref'   => { 'custnum' => $custnum },
   'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
 } );
 
-my @errors;
-if($cgi->param('now_or_later')) {
+if ( $error ) {
+  # do nothing
+} elsif ( $cgi->param('now_or_later') ) {
   $expire = parse_datetime($expire);
   if($expire) {
     #warn "setting expire dates on custnum#$custnum\n";
     my @pkgs = $cust_main->ncancelled_pkgs;
-    @errors = grep {$_} map { $_->cancel(
+    my @errors = grep {$_} map { $_->cancel(
       'reason'  => $reasonnum,
       'date'    => $expire,
     ) } @pkgs;
+    $error = join(' / ', @errors);
   }
   else {
-    @errors = ("error parsing expire date: ".$cgi->param('expire'));
+    $error = ("error parsing expire date: ".$cgi->param('expire'));
   }
 }
 else {
   warn "cancelling $cust_main";
-  @errors = $cust_main->cancel(
+  $error = $cust_main->cancel(
     'ban'    => $ban,
     'reason' => $reasonnum,
   );
 }
-my $error = join(' / ', @errors) if scalar(@errors);
 
 if ( $error ) {
   $cgi->param('error', $error);
index 6185136..7a501d6 100755 (executable)
@@ -22,50 +22,39 @@ if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
   $custnum = $1;
 }
 
-#false laziness w/process/cancel_pkg.html
-
-#untaint reasonnum
-my $reasonnum = $cgi->param('reasonnum');
-$reasonnum =~ /^(-?\d+)$/ || die "Illegal reasonnum";
-$reasonnum = $1;
-
-if ($reasonnum == -1) {
-  $reasonnum = {
-    'typenum' => scalar( $cgi->param('newreasonnumT') ),
-    'reason'  => scalar( $cgi->param('newreasonnum' ) ),
-  };
+#untaint reasonnum / create new reason
+my ($reasonnum, $error) = $m->comp('process/elements/reason');
+if (!$reasonnum) {
+  $error ||= 'Reason required';
 }
 
-#eslaf
-
 my $cust_main = qsearchs( {
   'table'     => 'cust_main',
   'hashref'   => { 'custnum' => $custnum },
   'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
 } );
 
-my @errors;
-if($cgi->param('now_or_later')) {
+if ( $error ) {
+  # do nothing
+} elsif ( $cgi->param('now_or_later') ) {
   $adjourn = parse_datetime($adjourn);
   if($adjourn) {
     #warn "setting adjourn dates on custnum#$custnum\n";
     my @pkgs = $cust_main->unsuspended_pkgs;
-    @errors = grep {$_} map { $_->suspend(
+    my @errors = grep {$_} map { $_->suspend(
       'reason'  => $reasonnum,
       'date'    => $adjourn,
     ) } @pkgs;
+    $error = join(' / ', @errors);
+  } else {
+    $error = ("error parsing adjourn date: ".$cgi->param('adjourn'));
   }
-  else {
-    @errors = ("error parsing adjourn date: ".$cgi->param('adjourn'));
-  }
-}
-else {
+} else {
   warn "suspending $cust_main";
-  @errors = $cust_main->suspend(
+  $error = $cust_main->suspend(
     'reason' => $reasonnum,
   );
 }
-my $error = join(' / ', @errors) if scalar(@errors);
 
 if ( $error ) {
   $cgi->param('error', $error);
index 27b45e0..0db4e25 100644 (file)
@@ -156,8 +156,7 @@ function save_changes() {
   }
   parent_input.value = JSON.stringify(data);
   // update the display
-  parent.show_fcc_options(parent_input.id);
-  parent.cClick(); //overlib
+  parent.finish_edit_fcc(parent_input.id);
 }
 
 function enable_fieldset(fieldset_id) {
index a106b84..47ceca2 100755 (executable)
@@ -65,17 +65,12 @@ if ( $method eq 'suspend' ) { #or 'adjourn'
 
 my $cust_pkg = qsearchs( 'cust_pkg', {'pkgnum'=>$pkgnum} );
 
-#untaint reasonnum
-my $reasonnum = $cgi->param('reasonnum');
-if ( $method !~ /^(unsuspend|uncancel)$/ ) {
-  $reasonnum =~ /^(-?\d+)$/ or die "Illegal reasonnum";
-  $reasonnum = $1;
-
-  if ($reasonnum == -1) {
-    $reasonnum = {
-      'typenum' => scalar( $cgi->param('newreasonnumT') ),
-      'reason'  => scalar( $cgi->param('newreasonnum' ) ),
-    };
+#untaint reasonnum, and set up new reason if appropriate
+my ($reasonnum, $error);
+if ($method ne 'resume' and $method ne 'uncancel') {
+  ($reasonnum, $error) = $m->comp('elements/reason');
+  if (!$reasonnum) {
+    $error ||= 'Reason required';
   }
 }
 
@@ -87,7 +82,7 @@ my $bill =
 
 my $svc_fatal = ( $cgi->param('svc_not_fatal') ne 'Y' );
 
-my $error = $cust_pkg->$method( 'reason'      => $reasonnum,
+$error ||=  $cust_pkg->$method( 'reason'      => $reasonnum,
                                 'date'        => $date,
                                 'resume_date' => $resume_date,
                                 'last_bill'   => $last_bill,
diff --git a/httemplate/misc/process/elements/reason b/httemplate/misc/process/elements/reason
new file mode 100644 (file)
index 0000000..ae92a75
--- /dev/null
@@ -0,0 +1,17 @@
+<%init>
+#untaint reasonnum, and set up new reason if appropriate
+my $reasonnum = $cgi->param('reasonnum');
+$reasonnum =~ /^(-?\d+)$/ or die "Illegal reasonnum";
+$reasonnum = $1;
+
+my $error;
+if ($reasonnum == -1) {
+  my $new_reason = FS::reason->new({
+    map { $_ => scalar( $cgi->param("reasonnum_new_$_") ) }
+    qw( reason_type reason unsuspend_pkgpart unsuspend_hold unused_credit )
+  }); # not sanitizing them here, but check() will do it
+  $error = $new_reason->insert;
+  $reasonnum = $new_reason->reasonnum;
+}
+return ($reasonnum, $error);
+</%init>
index 8eedd0a..83d9743 100644 (file)
@@ -40,7 +40,7 @@ toggle(false);
              'field'          => 'reasonnum',
              'reason_class'   => 'S',
              'cgi'            => $cgi,
-             'control_button' => "document.getElementById('confirm_suspend_cust_button')",
+             'control_button' => 'confirm_suspend_cust_button',
 &>
 
 </TABLE>
index 6182653..d0255a0 100644 (file)
@@ -43,6 +43,10 @@ foreach my $pre ( @prefixes ) {
     last if !$all_same;
   }
 
+  $all_same = 0 if ( length( $old{$pre.'censustract'} ) > 0 &&
+                     length( $new{$pre.'censustract'} ) > 0 &&
+                     $old{$pre.'censustract'} ne $new{$pre.'censustract'} );
+
   $all_same = 0 if $new{$pre.'error'};
 }
 
index 244bfa1..ff2ac86 100644 (file)
@@ -212,7 +212,7 @@ my $part_titles = FS::Report::FCC_477->parts;
 </%def>
 <%def .header_voip>
   <TR CLASS="head">
-    <TD ROWSPAN=2>State</TD>
+    <TD ROWSPAN=3>State</TD>
     <TD COLSPAN=2>VoIP OTT</TD>
     <TD COLSPAN=8>VoIP Non-OTT</TD>
   </TR>
diff --git a/httemplate/search/svc_circuit.cgi b/httemplate/search/svc_circuit.cgi
new file mode 100755 (executable)
index 0000000..c14c55f
--- /dev/null
@@ -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>
index 4880ac3..833b6d0 100755 (executable)
@@ -60,8 +60,8 @@ function areyousure(href, message) {
                 'actionlabel' => emt('Confirm Suspension'),
                 'color'       => '#ff9900',
                 'cust_main'   => $cust_main,
-                'width'       => 616, #make room for reasons
-                'height'      => 366,
+                'width'       => 768, #make room for reasons
+                'height'      => 450, 
               }
   &> | 
 % }
@@ -91,7 +91,7 @@ function areyousure(href, message) {
                 'color'       => '#ff0000',
                 'cust_main'   => $cust_main,
                 'width'       => 616, #make room for reasons
-                'height'      => 366,
+                'height'      => 410,
               }
   &> | 
 % }
index fdbbc39..336c1aa 100755 (executable)
@@ -35,16 +35,18 @@ table.location {
 <TH COLSPAN=5>
 <DIV CLASS="<% $loc->disabled ? 'loclabel disabled' : 'loclabel' %>">
 <% $loc->location_label %>
-%   if ( $loc->censustract ) {
-        <BR>
-        <FONT SIZE=-1>
-        <% $loc->censustract %> (<% $loc->censusyear %> census)
-        </FONT>
-%   } elsif ( $conf->exists('cust_main-require_censustract') ) {
-        <BR>
-        <FONT SIZE=-1 COLOR="#ee3300">
-        <% emt('Census tract unknown') %>
-        </FONT>
+%   if ( $loc->country eq 'US' ) { # only U.S. census tracts for now
+%     if ( $loc->censustract ) {
+          <BR>
+          <FONT SIZE=-1>
+          <% $loc->censustract %> (<% $loc->censusyear %> census)
+          </FONT>
+%     } elsif ( $conf->exists('cust_main-require_censustract') ) {
+          <BR>
+          <FONT SIZE=-1 COLOR="#ee3300">
+          <% emt('Census tract unknown') %>
+          </FONT>
+%     }
 %   }
 </DIV>
 <DIV STYLE="display: inline; float:right;">
index c5ef10e..99d91e5 100644 (file)
           &>
           </FONT>
 %     }
-%     if ( $loc->censustract ) {
-         <BR>
-         <FONT SIZE=-1>
-         <% $loc->censustract %> (<% $loc->censusyear %> census)
-         </FONT>
-%     } elsif ( $opt{'cust_main-require_censustract'} ) {
-          <BR>
-          <FONT SIZE=-1 COLOR="#ee3300">
-          <% emt('Census tract unknown') %>
-          </FONT>
+%     if ( $loc->country eq 'US' ) {
+%       if ( $loc->censustract ) {
+           <BR>
+           <FONT SIZE=-1>
+           <% $loc->censustract %> (<% $loc->censusyear %> census)
+           </FONT>
+%       } elsif ( $opt{'cust_main-require_censustract'} ) {
+            <BR>
+            <FONT SIZE=-1 COLOR="#ee3300">
+            <% emt('Census tract unknown') %>
+            </FONT>
+%       }
 %     }
 
 %     if ( $default ) {
index accdb45..f760d6f 100644 (file)
@@ -582,6 +582,7 @@ sub pkg_suspend_link {
              'actionlabel' => emt('Suspend'),
              'color'       => '#FF9900',
              'cust_pkg'    => shift,
+             'height'      => 420,
          )
 }
 
@@ -592,6 +593,7 @@ sub pkg_adjourn_link {
              'actionlabel' => emt('Adjourn'),
              'color'       => '#CC6600',
              'cust_pkg'    => shift,
+             'height'      => 445,
          )
 }
 
index b12d2dd..6c5c902 100644 (file)
@@ -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 (file)
index 0000000..c8d5d23
--- /dev/null
@@ -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>
index 2a2ef24..1c0fb39 100644 (file)
@@ -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;
 
index 7c77015..5fdac15 100644 (file)
@@ -121,8 +121,7 @@ sub LimitToChildType  {
     my $self = shift;
     my $lookup = shift;
 
-    $self->Limit( FIELD => 'LookupType', VALUE => "$lookup" );
-    $self->Limit( FIELD => 'LookupType', ENDSWITH => "$lookup" );
+    $self->Limit( FIELD => 'LookupType', VALUE => "$lookup", OPERATOR => "ENDSWITH" );
 }
 
 
@@ -137,8 +136,7 @@ sub LimitToParentType  {
     my $self = shift;
     my $lookup = shift;
 
-    $self->Limit( FIELD => 'LookupType', VALUE => "$lookup" );
-    $self->Limit( FIELD => 'LookupType', STARTSWITH => "$lookup" );
+    $self->Limit( FIELD => 'LookupType', VALUE => "$lookup", OPERATOR => "STARTSWITH" );
 }
 
 =head2 LimitToObjectId
index 3fe51c1..f632e68 100644 (file)
 % if ($Table) {
 <table>
 % }
-% while ( my $CustomField = $CustomFields->Next ) {
-% my $Values = $Object->CustomFieldValues( $CustomField->Id );
-% my $count = $Values->Count;
+% foreach my $set ($CustomFields, $HiddenCustomFields) {
+%   $set->GotoFirstItem;
+%   while ( my $CustomField = $set->Next ) {
+%     my $Values = $Object->CustomFieldValues( $CustomField->Id );
+%     my $count = $Values->Count;
+%     next if $count == 0 and $CustomField->Disabled;
   <tr id="CF-<%$CustomField->id%>-ShowRow">
     <td class="label"><% $CustomField->Name %>:</td>
     <td class="value">
-% unless ( $count ) {
+%     if ( $count == 0 ) {
 <i><&|/l&>(no value)</&></i>
-% } elsif ( $count == 1 ) {
-%   $print_value->( $CustomField, $Values->First );
-% } else {
+%     } elsif ( $count == 1 ) {
+%       $print_value->( $CustomField, $Values->First );
+%     } else {
 <ul>
-% while ( my $Value = $Values->Next ) {
+%       while ( my $Value = $Values->Next ) {
 <li>
-% $print_value->( $CustomField, $Value );
+%         $print_value->( $CustomField, $Value );
 </li>
-% }
+%       }
 </ul>
-% }
+%     }
     </td>
   </tr>
+%   }
 % }
 % if ($Table) {
 </table>
@@ -83,9 +87,15 @@ $m->callback(
     CustomFields => $CustomFields,
 );
 
+# kludge to allow "Support time" to be displayed even though it's been
+# removed
+my $HiddenCustomFields = RT::CustomFields->new($session{'CurrentUser'});
+$HiddenCustomFields->LimitToChildType(ref $Object);
+$HiddenCustomFields->Limit( FIELD => 'Type', VALUE => 'TimeValue' );
+$HiddenCustomFields->LimitToDeleted;
+
 # don't print anything if there is no custom fields
-return unless $CustomFields->First;
-$CustomFields->GotoFirstItem;
+return unless $CustomFields->Count > 0 or $HiddenCustomFields->Count > 0;
 
 my $print_value = sub {
     my ($cf, $value) = @_;
@@ -127,5 +137,6 @@ my $print_value = sub {
 <%ARGS>
 $Object => undef
 $CustomFields => $Object->CustomFields
+
 $Table => 1
 </%ARGS>