Merge remote-tracking branch 'upstream/master'
authorRob Van Dam <rvandam00@gmail.com>
Thu, 7 Nov 2013 22:27:24 +0000 (15:27 -0700)
committerRob Van Dam <rvandam00@gmail.com>
Thu, 7 Nov 2013 22:27:24 +0000 (15:27 -0700)
Conflicts:
FS/FS/cust_pkg.pm

75 files changed:
FS/FS.pm
FS/FS/ClientAPI/MyAccount.pm
FS/FS/Conf.pm
FS/FS/Mason.pm
FS/FS/Misc/Geo.pm
FS/FS/Record.pm
FS/FS/Report/Table/Daily.pm
FS/FS/Schema.pm
FS/FS/Upgrade.pm
FS/FS/access_user.pm
FS/FS/contact.pm
FS/FS/contact_email.pm
FS/FS/contact_phone.pm
FS/FS/cust_bill.pm
FS/FS/cust_credit.pm
FS/FS/cust_main.pm
FS/FS/cust_main/Billing.pm
FS/FS/cust_main/Location.pm
FS/FS/cust_main/Search.pm
FS/FS/cust_pkg.pm
FS/FS/discount.pm
FS/FS/discount_class.pm [new file with mode: 0644]
FS/FS/part_pkg/prorate_calendar.pm [new file with mode: 0644]
FS/FS/payinfo_Mixin.pm
FS/FS/sales.pm
FS/FS/tower.pm
FS/FS/tower_sector.pm
FS/MANIFEST
FS/bin/freeside-upgrade
FS/t/discount_class.t [new file with mode: 0644]
eg/table_template.pm
httemplate/browse/discount.html
httemplate/browse/discount_class.html [new file with mode: 0644]
httemplate/browse/tower.html
httemplate/edit/access_user.html
httemplate/edit/discount.html
httemplate/edit/discount_class.html [new file with mode: 0644]
httemplate/edit/elements/class_Common.html
httemplate/edit/elements/svc_Common.html
httemplate/edit/process/discount_class.html [new file with mode: 0644]
httemplate/edit/process/tower.html
httemplate/edit/tower.html
httemplate/elements/contact.html
httemplate/elements/input-text.html
httemplate/elements/menu.html
httemplate/elements/select-discount_class.html [new file with mode: 0644]
httemplate/elements/standardize_locations.js
httemplate/elements/tower_sector.html
httemplate/elements/tr-input-beginning_ending.html
httemplate/elements/tr-input-mask.html
httemplate/elements/tr-password.html
httemplate/elements/tr-select-discount_class.html [new file with mode: 0644]
httemplate/graph/report_cust_bill_pkg_discount.html
httemplate/graph/report_money_time_daily.html
httemplate/loginout/login.html
httemplate/misc/confirm-address_standardize.html
httemplate/pref/pref-process.html
httemplate/pref/pref.html
httemplate/search/cust_bill_pkg.cgi
httemplate/search/cust_bill_pkg_discount.html
httemplate/search/cust_pkg.cgi
httemplate/search/cust_pkg_discount.html
httemplate/search/cust_tax_exempt_pkg.cgi
httemplate/search/report_cust_bill_pkg_discount.html
httemplate/search/report_cust_main.html
httemplate/search/report_cust_pkg.html
httemplate/search/report_cust_pkg_discount.html
httemplate/search/report_sales_commission.html
httemplate/search/report_tax.cgi
httemplate/search/report_tax.html
httemplate/search/sales_commission.html
httemplate/search/sales_pkg_class.html
httemplate/view/cust_main/billing.html
httemplate/view/cust_main/contacts_new.html
httemplate/view/svc_external.cgi

index b3ebf30..abba99b 100644 (file)
--- a/FS/FS.pm
+++ b/FS/FS.pm
@@ -312,7 +312,9 @@ L<FS:;cust_pkg_discount> - Customer package discount class
 
 L<FS:;cust_bill_pkg_discount> - Customer package discount line item application class
 
-L<FS:;discount> - Discount class
+L<FS::discount> - Discount class
+
+L<FS::discount_class> - Discount class class
 
 L<FS::reason_type> - Reason type class
 
index 77a4683..db50d42 100644 (file)
@@ -1011,7 +1011,7 @@ sub validate_payment {
 
   { 
     'cust_main'      => $cust_main, #XXX or just custnum??
-    'amount'         => $amount,
+    'amount'         => sprintf('%.2f', $amount),
     'payby'          => $payby,
     'payinfo'        => $payinfo,
     'paymask'        => $cust_main->mask_payinfo( $payby, $payinfo ),
index dd4cd68..eed84fc 100644 (file)
@@ -4259,9 +4259,9 @@ and customer address. Include units.',
   {
     'key'         => 'census_year',
     'section'     => 'UI',
-    'description' => 'The year to use in census tract lookups.  NOTE: you need to select 2012 for Year 2010 Census tract codes.  A selection of 2011 or 2010 provides Year 2000 Census tract codes.  Use the freeside-censustract-update tool if exisitng customers need to be changed.',
+    'description' => 'The year to use in census tract lookups.  NOTE: you need to select 2012 or 2013 for Year 2010 Census tract codes.  A selection of 2011 provides Year 2000 Census tract codes.  Use the freeside-censustract-update tool if exisitng customers need to be changed.',
     'type'        => 'select',
-    'select_enum' => [ qw( 2012 2011 2010 ) ],
+    'select_enum' => [ qw( 2013 2012 2011 ) ],
   },
 
   {
@@ -5239,7 +5239,7 @@ and customer address. Include units.',
   {
     'key'         => 'svc_phone-did-summary',
     'section'     => 'invoicing',
-    'description' => 'Enable DID activity summary on invoices, showing # DIDs activated/deactivated/ported-in/ported-out and total minutes usage, covering period since last invoice.',
+    'description' => 'Experimental feature to enable DID activity summary on invoices, showing # DIDs activated/deactivated/ported-in/ported-out and total minutes usage, covering period since last invoice.',
     'type'        => 'checkbox',
   },
 
index 398d785..fc25a86 100644 (file)
@@ -357,6 +357,7 @@ if ( -e $addl_handler_use_file ) {
   use FS::invoice_conf;
   use FS::cable_provider;
   use FS::cust_credit_void;
+  use FS::discount_class;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
index c6d6f1f..9f6b89b 100644 (file)
@@ -84,7 +84,7 @@ sub get_censustract_ffiec {
 
       my($zip5, $zip4) = split('-',$location->{zip});
 
-      $year ||= '2012';
+      $year ||= '2013';
       my @ffiec_args = (
         __VIEWSTATE => $viewstate,
         __EVENTVALIDATION => $eventvalidation,
@@ -414,13 +414,30 @@ sub standardize_ezlocate {
   \%result;
 }
 
+sub _tomtom_query { # helper method for the below
+  my %args = @_;
+  my $result = Geo::TomTom::Geocoding->query(%args);
+  die "TomTom geocoding error: ".$result->message."\n"
+    unless ( $result->is_success );
+  my ($match) = $result->locations;
+  my $type = $match->{type};
+  # match levels below "intersection" should not be considered clean
+  my $clean = ($type eq 'addresspoint'  ||
+               $type eq 'poi'           ||
+               $type eq 'house'         ||
+               $type eq 'intersection'
+              ) ? 'Y' : '';
+  warn "tomtom returned $type match\n" if $DEBUG;
+  warn Dumper($match) if $DEBUG > 1;
+  ($match, $clean);
+}
+
 sub standardize_tomtom {
   # post-2013 TomTom API
   # much better, but incompatible with ezlocate
   my $self = shift;
   my $location = shift;
-  my $class = 'Geo::TomTom::Geocoding';
-  eval "use $class";
+  eval "use Geo::TomTom::Geocoding; use Geo::StreetAddress::US";
   die $@ if $@;
 
   my $key = $conf->config('tomtom-userid')
@@ -428,12 +445,25 @@ sub standardize_tomtom {
 
   my $country = code2country($location->{country});
   my ($address1, $address2) = ($location->{address1}, $location->{address2});
+  my $subloc = '';
+
+  # trim whitespace
+  $address1 =~ s/^\s+//;
+  $address1 =~ s/\s+$//;
+  $address2 =~ s/^\s+//;
+  $address2 =~ s/\s+$//;
+
   # try to fix some cases of the address fields being switched
   if ( $address2 =~ /^\d/ and $address1 !~ /^\d/ ) {
     $address2 = $address1;
     $address1 = $location->{address2};
   }
-  my $result = $class->query(
+  # parse sublocation part (unit/suite/apartment...) and clean up 
+  # non-sublocation address2
+  ($subloc, $address2) =
+    subloc_address2($address1, $address2, $location->{country});
+  # ask TomTom to standardize address1:
+  my %args = (
     key => $key,
     T   => $address1,
     L   => $location->{city},
@@ -441,40 +471,48 @@ sub standardize_tomtom {
     PC  => $location->{zip},
     CC  => country2code($country, LOCALE_CODE_ALPHA_3),
   );
-  unless ( $result->is_success ) {
-    die "TomTom geocoding error: ".$result->message."\n";
+
+  my ($match, $clean) = _tomtom_query(%args);
+
+  if (!$match or !$clean) {
+    # Then try cleaning up the input; TomTom is picky about junk in the 
+    # address.  Any of these can still be a clean match.
+    my $h = Geo::StreetAddress::US->parse_location($address1);
+    # First conservatively:
+    if ( $h->{sec_unit_type} ) {
+      my $strip = '\s+' . $h->{sec_unit_type};
+      $strip .= '\s*' . $h->{sec_unit_num} if $h->{sec_unit_num};
+      $strip .= '$';
+      $args{T} =~ s/$strip//;
+      ($match, $clean) = _tomtom_query(%args);
+    }
+    if ( !$match or !$clean ) {
+      # Then more aggressively:
+      $args{T} = uc( join(' ', @$h{'number', 'street', 'type'}) );
+      ($match, $clean) = _tomtom_query(%args);
+    }
   }
-  my ($match) = $result->locations;
-  if (!$match) {
-    die "Location not found.\n";
+
+  if ( !$match or !$clean ) { # partial matches are not useful
+    die "Address not found\n";
   }
-  my $type = $match->{type};
-  warn "tomtom returned $type match\n" if $DEBUG;
-  warn Dumper($match) if $DEBUG > 1;
   my $tract = '';
   if ( defined $match->{censusTract} ) {
     $tract = $match->{censusStateCode}. $match->{censusFipsCountyCode}.
              join('.', $match->{censusTract} =~ /(....)(..)/);
   }
-  # match levels below "intersection" should not be considered clean
-  my $clean = ($type eq 'addresspoint'  ||
-               $type eq 'poi'           ||
-               $type eq 'house'         ||
-               $type eq 'intersection'
-              ) ? 'Y' : '';
-
-  $address2 = normalize_address2($address2, $location->{country});
-
   $address1 = '';
   $address1 = $match->{houseNumber} . ' ' if length($match->{houseNumber});
   $address1 .= $match->{street} if $match->{street};
+  $address1 .= ' '.$subloc if $subloc;
+  $address1 = uc($address1); # USPS standards
 
   return +{
     address1    => $address1,
     address2    => $address2,
-    city        => $match->{city},
-    state       => $location->{state},    # this will never change
-    country     => $location->{country},  # ditto
+    city        => uc($match->{city}),
+    state       => uc($location->{state}),
+    country     => uc($location->{country}),
     zip         => ($match->{standardPostalCode} || $match->{postcode}),
     latitude    => $match->{latitude},
     longitude   => $match->{longitude},
@@ -483,15 +521,16 @@ sub standardize_tomtom {
   };
 }
 
-=iten normalize_address2 STRING, COUNTRY
+=iten subloc_address2 ADDRESS1, ADDRESS2, COUNTRY
 
-Given an 'address2' STRING, normalize it for COUNTRY postal standards.
-Currently only works for US and CA.
+Given 'address1' and 'address2' strings, extract the sublocation part 
+(from either one) and return it.  If the sublocation was found in ADDRESS1,
+also return ADDRESS2 (cleaned up for postal standards) as it's assumed to
+contain something relevant.
 
 =cut
 
-# XXX really ought to be a separate module
-my %address2_forms = (
+my %subloc_forms = (
   # Postal Addressing Standards, Appendix C
   # (plus correction of "hanger" to "hangar")
   US => {qw(
@@ -532,26 +571,76 @@ my %address2_forms = (
   )},
 );
  
-sub normalize_address2 {
+sub subloc_address2 {
   # Some things seen in the address2 field:
   # Whitespace
   # The complete address (with address1 containing part of the company name, 
   # or an ATTN or DBA line, or P.O. Box, or department name, or building/suite
   # number, etc.)
-  my ($addr2, $country) = @_;
-  $addr2 = uc($addr2);
-  if ( exists($address2_forms{$country}) ) {
-    my $dict = $address2_forms{$country};
-    # protect this
-    $addr2 =~ s/#\s*(\d)/NUMBER$1/; # /g?
-    my @words;
-    # remove all punctuation and spaces
-    foreach my $w (split(/\W+/, $addr2)) {
-      if ( exists($dict->{$w}) ) {
-        push @words, $dict->{$w};
-      } else {
-        push @words, $w;
-      }
+
+  # try to parse sublocation parts from address1; if they are present we'll
+  # append them back to address1 after standardizing
+  my $subloc = '';
+  my ($addr1, $addr2, $country) = map uc, @_;
+  my $dict = $subloc_forms{$country} or return('', $addr2);
+  
+  my $found_in = 0; # which address is the sublocation
+  my $h;
+  foreach my $string (
+    # patterns to try to parse
+    $addr1,
+    "$addr1 Nullcity, CA"
+  ) {
+    $h = Geo::StreetAddress::US->parse_location($addr1);
+    last if exists($h->{sec_unit_type});
+  }
+  if (exists($h->{sec_unit_type})) {
+    $found_in = 1
+  } else {
+    foreach my $string (
+      # more patterns
+      $addr2,
+      "$addr1, $addr2",
+      "$addr1, $addr2 Nullcity, CA"
+    ) {
+      $h = Geo::StreetAddress::US->parse_location("$addr1, $addr2");
+      last if exists($h->{sec_unit_type});
+    }
+    if (exists($h->{sec_unit_type})) {
+      $found_in = 2;
+    }
+  }
+  if ( $found_in ) {
+    $subloc = $h->{sec_unit_type};
+    # special case: do not combine P.O. box sublocs with address1
+    if ( $h->{sec_unit_type} =~ /^P *O *BOX/i ) {
+      if ( $found_in == 2 ) {
+        $addr2 = "PO BOX ".$h->{sec_unit_num};
+      } # else it's in addr1, and leave it alone
+      return ('', $addr2);
+    } elsif ( exists($dict->{$subloc}) ) {
+      # substitute the official abbreviation
+      $subloc = $dict->{$subloc};
+    }
+    $subloc .= ' ' . $h->{sec_unit_num} if length($h->{sec_unit_num});
+  } # otherwise $subloc = ''
+
+  if ( $found_in == 2 ) {
+    # address2 should be fully combined into address1
+    return ($subloc, '');
+  }
+  # else address2 is not the canonical sublocation, but do our best to 
+  # clean it up
+  #
+  # protect this
+  $addr2 =~ s/#\s*(\d)/NUMBER$1/; # /g?
+  my @words;
+  # remove all punctuation and spaces
+  foreach my $w (split(/\W+/, $addr2)) {
+    if ( exists($dict->{$w}) ) {
+      push @words, $dict->{$w};
+    } else {
+      push @words, $w;
     }
     my $result = join(' ', @words);
     # correct spacing of pound sign + number
@@ -559,7 +648,8 @@ sub normalize_address2 {
     warn "normalizing '$addr2' to '$result'\n" if $DEBUG > 1;
     $addr2 = $result;
   }
-  $addr2;
+  $addr2 = '' if $addr2 eq $subloc; # if it was entered redundantly
+  ($subloc, $addr2);
 }
 
 
@@ -567,5 +657,4 @@ sub normalize_address2 {
 
 =cut
 
-
 1;
index 71eddc1..1a88c3a 100644 (file)
@@ -1,22 +1,18 @@
 package FS::Record;
+use base qw( Exporter );
 
 use strict;
-use vars qw( $AUTOLOAD @ISA @EXPORT_OK $DEBUG
+use vars qw( $AUTOLOAD
              %virtual_fields_cache
-             $conf $conf_encryption $money_char $lat_lower $lon_upper
-             $me
-             $nowarn_identical $nowarn_classload
-             $no_update_diff $no_check_foreign
-             @encrypt_payby
+             $money_char $lat_lower $lon_upper
            );
-use Exporter;
 use Carp qw(carp cluck croak confess);
 use Scalar::Util qw( blessed );
 use File::Slurp qw( slurp );
 use File::CounterFile;
 use Text::CSV_XS;
 use DBI qw(:sql_types);
-use DBIx::DBSchema 0.38;
+use DBIx::DBSchema 0.43; #0.43 for foreign keys
 use Locale::Country;
 use Locale::Currency;
 use NetAddr::IP; # for validation
@@ -31,32 +27,31 @@ use FS::part_virtual_field;
 
 use Tie::IxHash;
 
-@ISA = qw(Exporter);
-
-@encrypt_payby = qw( CARD DCRD CHEK DCHK );
+our @encrypt_payby = qw( CARD DCRD CHEK DCHK );
 
 #export dbdef for now... everything else expects to find it here
-@EXPORT_OK = qw(
+our @EXPORT_OK = qw(
   dbh fields hfields qsearch qsearchs dbdef jsearch
   str2time_sql str2time_sql_closing regexp_sql not_regexp_sql concat_sql
   midnight_sql
 );
 
-$DEBUG = 0;
-$me = '[FS::Record]';
+our $DEBUG = 0;
+our $me = '[FS::Record]';
+
+our $nowarn_identical = 0;
+our $nowarn_classload = 0;
+our $no_update_diff = 0;
 
-$nowarn_identical = 0;
-$nowarn_classload = 0;
-$no_update_diff = 0;
-$no_check_foreign = 0;
+our $no_check_foreign = 1; #well, not inefficiently in perl by default anymore
 
 my $rsa_module;
 my $rsa_loaded;
 my $rsa_encrypt;
 my $rsa_decrypt;
 
-$conf = '';
-$conf_encryption = '';
+our $conf = '';
+our $conf_encryption = '';
 FS::UID->install_callback( sub {
 
   eval "use FS::Conf;";
index 6087b0d..570fefe 100644 (file)
@@ -27,6 +27,7 @@ FS::Report::Table::Daily - Tables of report data, indexed daily
     'end_day'     => 27,
     #opt
     'agentnum'    => 54
+    'cust_classnum' => [ 1,2,4 ],
     'params'      => [ [ 'paramsfor', 'item_one' ], [ 'item', 'two' ] ], # ...
     'remove_empty' => 1, #collapse empty rows, default 0
     'item_labels' => [ ], #useful with remove_empty
@@ -54,6 +55,8 @@ sub data {
   my $emonth = $self->{'end_month'};
   my $eyear = $self->{'end_year'};
   my $agentnum = $self->{'agentnum'};
+  my $cust_classnum = $self->{'cust_classnum'} || [];
+  $cust_classnum = [ $cust_classnum ] if !ref($cust_classnum);
 
   my %data;
 
@@ -83,6 +86,7 @@ sub data {
     for ( $i = 0; $i < scalar(@items); $i++ ) {
          my $item = $items[$i];
          my @param = $self->{'params'} ? @{ $self->{'params'}[$col] }: ();
+          push @param, 'cust_classnum' => $cust_classnum if @$cust_classnum;
          my $value = $self->$item($speriod, $eperiod, $agentnum, @param);
          push @{$data{data}->[$col++]}, $value;
     }
index 3029ab5..8ba6020 100644 (file)
@@ -3,10 +3,11 @@ package FS::Schema;
 use vars qw(@ISA @EXPORT_OK $DEBUG $setup_hack %dbdef_cache);
 use subs qw(reload_dbdef);
 use Exporter;
-use DBIx::DBSchema 0.40; #0.40 for mysql upgrade fixes
+use DBIx::DBSchema 0.43; #0.43 for foreign keys
 use DBIx::DBSchema::Table;
 use DBIx::DBSchema::Column;
 use DBIx::DBSchema::Index;
+use DBIx::DBSchema::ForeignKey;
 #can't use this yet, dependency bs #use FS::Conf;
 
 @ISA = qw(Exporter);
@@ -149,12 +150,17 @@ sub dbdef_dist {
                        }
                        @index;
 
+    my @foreign_keys =
+      map DBIx::DBSchema::ForeignKey->new($_),
+        @{ $tables_hashref->{$tablename}{'foreign_keys'} || [] };
+
     DBIx::DBSchema::Table->new({
-      'name'          => $tablename,
-      'primary_key'   => $tables_hashref->{$tablename}{'primary_key'},
-      'columns'       => \@columns,
-      'indices'       => \@indices,
-      'local_options' => $local_options,
+      name          => $tablename,
+      primary_key   => $tables_hashref->{$tablename}{'primary_key'},
+      columns       => \@columns,
+      indices       => \@indices,
+      foreign_keys  => \@foreign_keys,
+      local_options => $local_options,
     });
 
   } keys %$tables_hashref;
@@ -204,7 +210,7 @@ sub dbdef_dist {
 
     my %h_indices = ();
 
-    unless ( $table eq 'cust_event' ) { #others?
+    unless ( $table eq 'cust_event' || $table eq 'cdr' ) { #others?
 
       my %indices = $tableobj->indices;
     
@@ -487,12 +493,28 @@ sub tables_hashref {
         'freq',              'int', 'NULL', '', '', '', #deprecated (never used)
         'prog',                     @perl_type, '', '', #deprecated (never used)
       ],
-      'primary_key' => 'agentnum',
+      'primary_key'  => 'agentnum',
       #'unique' => [ [ 'agent_custnum' ] ], #one agent per customer?
                                             #insert is giving it a value, tho..
       #'index' => [ ['typenum'], ['disabled'] ],
-      'unique' => [],
-      'index' => [ ['typenum'], ['disabled'], ['agent_custnum'] ],
+      'unique'       => [],
+      'index'        => [ ['typenum'], ['disabled'], ['agent_custnum'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'typenum' ],
+                            table      => 'agent_type',
+                          },
+                          # 1. RT tables aren't part of our data structure, so
+                          #     we can't make sure Queue is created already
+                          # 2. Future ability to plug in other ticketing systems
+                          #{ columns    => [ 'ticketing_queueid' ],
+                          #  table      => 'Queue',
+                          #  references => [ 'id' ],
+                          #},
+                          { columns    => [ 'agent_custnum' ],
+                            table      => 'cust_main',
+                            references => [ 'custnum' ],
+                          },
+                        ],
     },
 
     'agent_pkg_class' => {
@@ -502,9 +524,17 @@ sub tables_hashref {
         'classnum',               'int', 'NULL',    '', '', '',
         'commission_percent', 'decimal',     '', '7,4', '', '',
       ],
-      'primary_key' => 'agentpkgclassnum',
-      'unique'      => [ [ 'agentnum', 'classnum' ], ],
-      'index'       => [],
+      'primary_key'  => 'agentpkgclassnum',
+      'unique'       => [ [ 'agentnum', 'classnum' ], ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                          { columns    => [ 'classnum' ],
+                            table      => 'pkg_class',
+                          },
+                        ],
     },
 
     'agent_type' => {
@@ -523,9 +553,17 @@ sub tables_hashref {
         'typenum',   'int',  '', '', '', '', 
         'pkgpart',   'int',  '', '', '', '', 
       ],
-      'primary_key' => 'typepkgnum',
-      'unique' => [ ['typenum', 'pkgpart'] ],
-      'index' => [ ['typenum'] ],
+      'primary_key'  => 'typepkgnum',
+      'unique'       => [ ['typenum', 'pkgpart'] ],
+      'index'        => [ ['typenum'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'typenum' ],
+                            table      => 'agent_type',
+                          },
+                          { columns    => [ 'pkgpart' ],
+                            table      => 'part_pkg',
+                          },
+                        ],
     },
 
     'agent_currency' => {
@@ -534,9 +572,14 @@ sub tables_hashref {
         'agentnum',            'int', '', '', '', '',
         'currency',           'char', '',  3, '', '',
       ],
-      'primary_key' => 'agentcurrencynum',
-      'unique'      => [],
-      'index'       => [ ['agentnum'] ],
+      'primary_key'  => 'agentcurrencynum',
+      'unique'       => [],
+      'index'        => [ ['agentnum'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                        ],
     },
 
     'sales' => {
@@ -547,9 +590,18 @@ sub tables_hashref {
         'sales_custnum',        'int', 'NULL',      '', '', '',
         'disabled',            'char', 'NULL',       1, '', '', 
       ],
-      'primary_key' => 'salesnum',
-      'unique' => [],
-      'index' => [ ['salesnum'], ['disabled'] ],
+      'primary_key'  => 'salesnum',
+      'unique'       => [],
+      'index'        => [ ['salesnum'], ['disabled'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                          { columns    => [ 'sales_custnum' ],
+                            table      => 'cust_main',
+                            references => [ 'custnum' ],
+                          },
+                        ],
     },
 
     'sales_pkg_class' => {
@@ -560,9 +612,17 @@ sub tables_hashref {
         'commission_percent', 'decimal',     '', '7,4', '', '',
         'commission_duration',    'int', 'NULL',    '', '', '',
       ],
-      'primary_key' => 'salespkgclassnum',
-      'unique'      => [ [ 'salesnum', 'classnum' ], ],
-      'index'       => [],
+      'primary_key'  => 'salespkgclassnum',
+      'unique'       => [ [ 'salesnum', 'classnum' ], ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'salesnum' ],
+                            table      => 'sales',
+                          },
+                          { columns    => [ 'classnum' ],
+                            table      => 'pkg_class',
+                          },
+                        ],
     },
 
     'cust_attachment' => {
@@ -578,10 +638,18 @@ sub tables_hashref {
         'body',      'blob', 'NULL', '', '', '',
         'disabled',  @date_type, '', '',
       ],
-      'primary_key' => 'attachnum',
-      'unique'      => [],
-      'index'       => [ ['custnum'], ['usernum'], ],
-    },
+      'primary_key'  => 'attachnum',
+      'unique'       => [],
+      'index'        => [ ['custnum'], ['usernum'], ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                        ],
+   },
 
     'cust_bill' => {
       'columns' => [
@@ -606,9 +674,19 @@ sub tables_hashref {
         'agent_invid',  'int', 'NULL', '', '', '', #(varchar?) importing legacy
         'promised_date', @date_type,       '', '',
       ],
-      'primary_key' => 'invnum',
-      'unique' => [ [ 'custnum', 'agent_invid' ] ], #agentnum?  huh
-      'index' => [ ['custnum'], ['_date'], ['statementnum'], ['agent_invid'] ],
+      'primary_key'  => 'invnum',
+      'unique'       => [ [ 'custnum', 'agent_invid' ] ], #agentnum?  huh
+      'index'        => [ ['custnum'], ['_date'], ['statementnum'],
+                          ['agent_invid'],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'statementnum' ],
+                            table      => 'cust_statement',
+                          },
+                        ],
     },
 
     'cust_bill_void' => {
@@ -636,9 +714,23 @@ sub tables_hashref {
         'reason',    'varchar',   'NULL', $char_d, '', '', 
         'void_usernum',   'int', 'NULL', '', '', '',
       ],
-      'primary_key' => 'invnum',
-      'unique' => [ [ 'custnum', 'agent_invid' ] ], #agentnum?  huh
-      'index' => [ ['custnum'], ['_date'], ['statementnum'], ['agent_invid'], [ 'void_usernum' ] ],
+      'primary_key'  => 'invnum',
+      'unique'       => [ [ 'custnum', 'agent_invid' ] ], #agentnum?  huh
+      'index'        => [ ['custnum'], ['_date'], ['statementnum'],
+                          ['agent_invid'], [ 'void_usernum' ],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'statementnum' ],
+                            table      => 'cust_statement', #_void? both?
+                          },
+                          { columns    => [ 'void_usernum' ],
+                            table      => 'access_user',
+                            references => [ 'usernum' ],
+                          },
+                        ],
     },
 
     #for importing invoices from a legacy system for display purposes only
@@ -655,9 +747,14 @@ sub tables_hashref {
         'content_html',    'text', 'NULL',      '', '', '',
         'locale',       'varchar', 'NULL',      16, '', '', 
       ],
-      'primary_key' => 'legacyinvnum',
-      'unique' => [],
-      'index'  => [ ['legacyid', 'custnum', 'locale' ], ],
+      'primary_key'  => 'legacyinvnum',
+      'unique'       => [],
+      'index'        => [ ['legacyid', 'custnum', 'locale' ], ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                        ],
     },
 
     'cust_statement' => {
@@ -666,11 +763,17 @@ sub tables_hashref {
         'custnum',         'int', '', '', '', '',
         '_date',           @date_type,    '', '',
       ],
-      'primary_key' => 'statementnum',
-      'unique' => [],
-      'index' => [ ['custnum'], ['_date'], ],
+      'primary_key'  => 'statementnum',
+      'unique'       => [],
+      'index'        => [ ['custnum'], ['_date'], ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                        ],
     },
 
+    #old "invoice" events, deprecated
     'cust_bill_event' => {
       'columns' => [
         'eventnum',    'serial',  '', '', '', '', 
@@ -680,14 +783,23 @@ sub tables_hashref {
         'status', 'varchar', '', $char_d, '', '', 
         'statustext', 'text', 'NULL', '', '', '', 
       ],
-      'primary_key' => 'eventnum',
+      'primary_key'  => 'eventnum',
       #no... there are retries now #'unique' => [ [ 'eventpart', 'invnum' ] ],
-      'unique' => [],
-      'index' => [ ['invnum'], ['status'], ['eventpart'],
-                   ['statustext'], ['_date'],
-                 ],
+      'unique'       => [],
+      'index'        => [ ['invnum'], ['status'], ['eventpart'],
+                          ['statustext'], ['_date'],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'invnum' ],
+                            table      => 'cust_bill',
+                          },
+                          { columns    => [ 'eventpart' ],
+                            table      => 'part_bill_event',
+                          },
+                        ],
     },
 
+    #old "invoice" events, deprecated
     'part_bill_event' => {
       'columns' => [
         'eventpart',    'serial',  '', '', '', '', 
@@ -702,9 +814,15 @@ sub tables_hashref {
         'reason',     'int', 'NULL', '', '', '', 
         'disabled',     'char', 'NULL', 1, '', '', 
       ],
-      'primary_key' => 'eventpart',
-      'unique' => [],
-      'index' => [ ['payby'], ['disabled'], ],
+      'primary_key'  => 'eventpart',
+      'unique'       => [],
+      'index'        => [ ['payby'], ['disabled'], ],
+      'foreign_keys' => [
+                          { columns    => [ 'reason' ],
+                            table      => 'reason',
+                            references => [ 'reasonnum' ],
+                          },
+                        ],
     },
 
     'part_event' => {
@@ -718,9 +836,16 @@ sub tables_hashref {
         'action',      'varchar',     '', $char_d, '', '',
         'disabled',     'char',   'NULL',       1, '', '', 
       ],
-      'primary_key' => 'eventpart',
-      'unique' => [],
-      'index' => [ ['agentnum'], ['eventtable'], ['check_freq'], ['disabled'], ],
+      'primary_key'  => 'eventpart',
+      'unique'       => [],
+      'index'        => [ ['agentnum'], ['eventtable'], ['check_freq'],
+                          ['disabled'],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                        ],
     },
 
     'part_event_option' => {
@@ -730,9 +855,14 @@ sub tables_hashref {
         'optionname', 'varchar', '', $char_d, '', '', 
         'optionvalue', 'text', 'NULL', '', '', '', 
       ],
-      'primary_key' => 'optionnum',
-      'unique'      => [],
-      'index'       => [ [ 'eventpart' ], [ 'optionname' ] ],
+      'primary_key'  => 'optionnum',
+      'unique'       => [],
+      'index'        => [ [ 'eventpart' ], [ 'optionname' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'eventpart' ],
+                            table      => 'part_event',
+                          },
+                        ],
     },
 
     'part_event_condition' => {
@@ -741,9 +871,14 @@ sub tables_hashref {
         'eventpart', 'int', '', '', '', '', 
         'conditionname', 'varchar', '', $char_d, '', '', 
       ],
-      'primary_key' => 'eventconditionnum',
-      'unique'      => [],
-      'index'       => [ [ 'eventpart' ], [ 'conditionname' ] ],
+      'primary_key'  => 'eventconditionnum',
+      'unique'       => [],
+      'index'        => [ [ 'eventpart' ], [ 'conditionname' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'eventpart' ],
+                            table      => 'part_event',
+                          },
+                        ],
     },
 
     'part_event_condition_option' => {
@@ -753,9 +888,14 @@ sub tables_hashref {
         'optionname', 'varchar', '', $char_d, '', '', 
         'optionvalue', 'text', 'NULL', '', '', '', 
       ],
-      'primary_key' => 'optionnum',
-      'unique'      => [],
-      'index'       => [ [ 'eventconditionnum' ], [ 'optionname' ] ],
+      'primary_key'  => 'optionnum',
+      'unique'       => [],
+      'index'        => [ [ 'eventconditionnum' ], [ 'optionname' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'eventconditionnum' ],
+                            table      => 'part_event_condition',
+                          },
+                        ],
     },
 
     'part_event_condition_option_option' => {
@@ -765,9 +905,14 @@ sub tables_hashref {
         'optionname', 'varchar', '', $char_d, '', '', 
         'optionvalue', 'text', 'NULL', '', '', '', 
       ],
-      'primary_key' => 'optionoptionnum',
-      'unique'      => [],
-      'index'       => [ [ 'optionnum' ], [ 'optionname' ] ],
+      'primary_key'  => 'optionoptionnum',
+      'unique'       => [],
+      'index'        => [ [ 'optionnum' ], [ 'optionname' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'optionnum' ],
+                            table      => 'part_event_condition_option',
+                          },
+                        ],
     },
 
     'cust_event' => {
@@ -779,12 +924,17 @@ sub tables_hashref {
         'status', 'varchar', '', $char_d, '', '', 
         'statustext', 'text', 'NULL', '', '', '', 
       ],
-      'primary_key' => 'eventnum',
+      'primary_key'  => 'eventnum',
       #no... there are retries now #'unique' => [ [ 'eventpart', 'invnum' ] ],
-      'unique' => [],
-      'index' => [ ['eventpart'], ['tablenum'], ['status'],
-                   ['statustext'], ['_date'],
-                 ],
+      'unique'       => [],
+      'index'        => [ ['eventpart'], ['tablenum'], ['status'],
+                          ['statustext'], ['_date'],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'eventpart' ],
+                            table      => 'part_event',
+                          },
+                        ],
     },
 
     'cust_bill_pkg' => {
@@ -810,9 +960,22 @@ sub tables_hashref {
         'quantity',               'int', 'NULL',      '', '', '',
         'hidden',                'char', 'NULL',       1, '', '',
       ],
-      'primary_key' => 'billpkgnum',
-      'unique' => [],
-      'index' => [ ['invnum'], [ 'pkgnum' ], [ 'itemdesc' ], ],
+      'primary_key'  => 'billpkgnum',
+      'unique'       => [],
+      'index'        => [ ['invnum'], [ 'pkgnum' ], [ 'itemdesc' ], ],
+      'foreign_keys' => [
+                          { columns    => [ 'invnum' ],
+                            table      => 'cust_bill',
+                          },
+                          #pkgnum 0 and -1 are used for special things
+                          #{ columns    => [ 'pkgnum' ],
+                          #  table      => 'cust_pkg',
+                          #},
+                          { columns    => [ 'pkgpart_override' ],
+                            table      => 'part_pkg',
+                            references => [ 'pkgpart' ],
+                          },
+                        ],
     },
 
     'cust_bill_pkg_detail' => {
@@ -831,9 +994,25 @@ sub tables_hashref {
         'regionname', 'varchar', 'NULL', $char_d, '', '',
         'detail',  'varchar', '', 255, '', '', 
       ],
-      'primary_key' => 'detailnum',
-      'unique' => [],
-      'index' => [ [ 'billpkgnum' ], [ 'classnum' ], [ 'pkgnum', 'invnum' ] ],
+      'primary_key'  => 'detailnum',
+      'unique'       => [],
+      'index'        => [ [ 'billpkgnum' ], [ 'classnum' ],
+                          [ 'pkgnum', 'invnum' ],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'billpkgnum' ],
+                            table      => 'cust_bill_pkg',
+                          },
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                          { columns    => [ 'invnum' ],
+                            table      => 'cust_bill',
+                          },
+                          { columns    => [ 'classnum' ],
+                            table      => 'usage_class',
+                          },
+                        ],
     },
 
     'cust_bill_pkg_display' => {
@@ -847,9 +1026,14 @@ sub tables_hashref {
         'type',       'char', 'NULL', 1, '', '',
         'summary',    'char', 'NULL', 1, '', '',
       ],
-      'primary_key' => 'billpkgdisplaynum',
-      'unique' => [],
-      'index' => [ ['billpkgnum'], ],
+      'primary_key'  => 'billpkgdisplaynum',
+      'unique'       => [],
+      'index'        => [ ['billpkgnum'], ],
+      'foreign_keys' => [
+                          { columns    => [ 'billpkgnum' ],
+                            table      => 'cust_bill_pkg',
+                          },
+                        ],
     },
 
     'cust_bill_pkg_tax_location' => {
@@ -864,14 +1048,29 @@ sub tables_hashref {
         'currency',                'char', 'NULL',       3, '', '',
         'taxable_billpkgnum',       'int', 'NULL',      '', '', '',
       ],
-      'primary_key' => 'billpkgtaxlocationnum',
-      'unique' => [],
-      'index'  => [ [ 'billpkgnum' ], 
-                    [ 'taxnum' ],
-                    [ 'pkgnum' ],
-                    [ 'locationnum' ],
-                    [ 'taxable_billpkgnum' ],
-                  ],
+      'primary_key'  => 'billpkgtaxlocationnum',
+      'unique'       => [],
+      'index'        => [ [ 'billpkgnum' ], 
+                          [ 'taxnum' ],
+                          [ 'pkgnum' ],
+                          [ 'locationnum' ],
+                          [ 'taxable_billpkgnum' ],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'billpkgnum' ],
+                            table      => 'cust_bill_pkg',
+                          },
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                          { columns    => [ 'locationnum' ],
+                            table      => 'cust_location',
+                          },
+                          { columns    => [ 'taxable_billpkgnum' ],
+                            table      => 'cust_bill_pkg',
+                            references => [ 'billpkgnum' ],
+                          },
+                        ],
     },
 
     'cust_bill_pkg_tax_rate_location' => {
@@ -886,10 +1085,23 @@ sub tables_hashref {
         'currency',                    'char', 'NULL',        3, '', '',
         'taxable_billpkgnum',           'int', 'NULL',       '', '', '',
       ],
-      'primary_key' => 'billpkgtaxratelocationnum',
-      'unique' => [],
-      'index'  => [ [ 'billpkgnum' ], [ 'taxnum' ], [ 'taxratelocationnum' ],
-                    [ 'taxable_billpkgnum' ], ],
+      'primary_key'  => 'billpkgtaxratelocationnum',
+      'unique'       => [],
+      'index'        => [ ['billpkgnum'], ['taxnum'], ['taxratelocationnum'],
+                          ['taxable_billpkgnum'],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'billpkgnum' ],
+                            table      => 'cust_bill_pkg',
+                          },
+                          { columns    => [ 'taxratelocationnum' ],
+                            table      => 'tax_rate_location',
+                          },
+                          { columns    => [ 'taxable_billpkgnum' ],
+                            table      => 'cust_bill_pkg',
+                            references => [ 'billpkgnum' ],
+                          },
+                        ],
     },
 
     'cust_bill_pkg_void' => {
@@ -917,9 +1129,27 @@ sub tables_hashref {
         'reason',    'varchar',   'NULL', $char_d, '', '', 
         'void_usernum',   'int', 'NULL', '', '', '',
       ],
-      'primary_key' => 'billpkgnum',
-      'unique' => [],
-      'index' => [ ['invnum'], [ 'pkgnum' ], [ 'itemdesc' ], [ 'void_usernum' ], ],
+      'primary_key'  => 'billpkgnum',
+      'unique'       => [],
+      'index'        => [ ['invnum'], ['pkgnum'], ['itemdesc'],
+                          ['void_usernum'],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'invnum' ],
+                            table      => 'cust_bill_void',
+                          },
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                          { columns    => [ 'pkgpart_override' ],
+                            table      => 'part_pkg',
+                            references => [ 'pkgpart' ],
+                          },
+                          { columns    => [ 'void_usernum' ],
+                            table      => 'access_user',
+                            references => [ 'usernum' ],
+                          },
+                        ],
     },
 
     'cust_bill_pkg_detail_void' => {
@@ -938,9 +1168,23 @@ sub tables_hashref {
         'regionname', 'varchar', 'NULL', $char_d, '', '',
         'detail',  'varchar', '', 255, '', '', 
       ],
-      'primary_key' => 'detailnum',
-      'unique' => [],
-      'index' => [ [ 'billpkgnum' ], [ 'classnum' ], [ 'pkgnum', 'invnum' ] ],
+      'primary_key'  => 'detailnum',
+      'unique'       => [],
+      'index'        => [ ['billpkgnum'], ['classnum'], ['pkgnum', 'invnum'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'billpkgnum' ],
+                            table      => 'cust_bill_pkg_void',
+                          },
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                          { columns    => [ 'invnum' ],
+                            table      => 'cust_bill',
+                          },
+                          { columns    => [ 'classnum' ],
+                            table      => 'usage_class',
+                          },
+                        ],
     },
 
     'cust_bill_pkg_display_void' => {
@@ -954,9 +1198,14 @@ sub tables_hashref {
         'type',       'char', 'NULL', 1, '', '',
         'summary',    'char', 'NULL', 1, '', '',
       ],
-      'primary_key' => 'billpkgdisplaynum',
-      'unique' => [],
-      'index' => [ ['billpkgnum'], ],
+      'primary_key'  => 'billpkgdisplaynum',
+      'unique'       => [],
+      'index'        => [ ['billpkgnum'], ],
+      'foreign_keys' => [
+                          { columns    => [ 'billpkgnum' ],
+                            table      => 'cust_bill_pkg_void',
+                          },
+                        ],
     },
 
     'cust_bill_pkg_tax_location_void' => {
@@ -971,9 +1220,26 @@ sub tables_hashref {
         'currency',                'char', 'NULL',       3, '', '',
         'taxable_billpkgnum',       'int', 'NULL',      '', '', '',
       ],
-      'primary_key' => 'billpkgtaxlocationnum',
-      'unique' => [],
-      'index'  => [ [ 'billpkgnum' ], [ 'taxnum' ], [ 'pkgnum' ], [ 'locationnum' ] ],
+      'primary_key'  => 'billpkgtaxlocationnum',
+      'unique'       => [],
+      'index'        => [ ['billpkgnum'], ['taxnum'], ['pkgnum'],
+                          ['locationnum'],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'billpkgnum' ],
+                            table      => 'cust_bill_pkg_void',
+                          },
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                          { columns    => [ 'locationnum' ],
+                            table      => 'cust_location',
+                          },
+                          { columns    => [ 'taxable_billpkgnum' ],
+                            table      => 'cust_bill_pkg_void',
+                            references => [ 'billpkgnum' ],
+                          },
+                        ],
     },
 
     'cust_bill_pkg_tax_rate_location_void' => {
@@ -987,9 +1253,17 @@ sub tables_hashref {
         'amount',                 @money_type,                  '', '',
         'currency',                    'char', 'NULL',       3, '', '',
       ],
-      'primary_key' => 'billpkgtaxratelocationnum',
-      'unique' => [],
-      'index'  => [ [ 'billpkgnum' ], [ 'taxnum' ], [ 'taxratelocationnum' ] ],
+      'primary_key'  => 'billpkgtaxratelocationnum',
+      'unique'       => [],
+      'index'        => [ ['billpkgnum'], ['taxnum'], ['taxratelocationnum'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'billpkgnum' ],
+                            table      => 'cust_bill_pkg_void',
+                          },
+                          { columns    => [ 'taxratelocationnum' ],
+                            table      => 'tax_rate_location',
+                          },
+                        ],
     },
 
     'cust_credit' => {
@@ -1011,11 +1285,40 @@ sub tables_hashref {
         'commission_salesnum', 'int', 'NULL', '', '', '', #
         'commission_pkgnum',   'int', 'NULL', '', '', '', #
       ],
-      'primary_key' => 'crednum',
-      'unique' => [],
-      'index' => [ ['custnum'], ['_date'], ['usernum'], ['eventnum'],
-                   [ 'commission_salesnum' ],
-                 ],
+      'primary_key'  => 'crednum',
+      'unique'       => [],
+      'index'        => [ ['custnum'], ['_date'], ['usernum'], ['eventnum'],
+                          ['commission_salesnum'],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                          { columns    => [ 'reasonnum' ],
+                            table      => 'reason',
+                          },
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                          { columns    => [ 'eventnum' ],
+                            table      => 'cust_event',
+                          },
+                          { columns    => [ 'commission_agentnum' ],
+                            table      => 'agent',
+                            references => [ 'agentnum' ],
+                          },
+                          { columns    => [ 'commission_salesnum' ],
+                            table      => 'sales',
+                            references => [ 'salesnum' ],
+                          },
+                          { columns    => [ 'commission_pkgnum' ],
+                            table      => 'cust_pkg',
+                            references => [ 'pkgnum' ],
+                          },
+                        ],
     },
 
     'cust_credit_void' => {
@@ -1041,11 +1344,44 @@ sub tables_hashref {
         'void_reason', 'varchar', 'NULL', $char_d, '', '', 
         'void_usernum',    'int', 'NULL',      '', '', '',
       ],
-      'primary_key' => 'crednum',
-      'unique' => [],
-      'index' => [ ['custnum'], ['_date'], ['usernum'], ['eventnum'],
-                   [ 'commission_salesnum' ],
-                 ],
+      'primary_key'  => 'crednum',
+      'unique'       => [],
+      'index'        => [ ['custnum'], ['_date'], ['usernum'], ['eventnum'],
+                          ['commission_salesnum'],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                          { columns    => [ 'reasonnum' ],
+                            table      => 'reason',
+                          },
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                          { columns    => [ 'eventnum' ],
+                            table      => 'cust_event',
+                          },
+                          { columns    => [ 'commission_agentnum' ],
+                            table      => 'agent',
+                            references => [ 'agentnum' ],
+                          },
+                          { columns    => [ 'commission_salesnum' ],
+                            table      => 'sales',
+                            references => [ 'salesnum' ],
+                          },
+                          { columns    => [ 'commission_pkgnum' ],
+                            table      => 'cust_pkg',
+                            references => [ 'pkgnum' ],
+                          },
+                          { columns    => [ 'void_usernum' ],
+                            table      => 'access_user',
+                            references => [ 'usernum' ],
+                          },
+                        ],
     },
 
 
@@ -1058,9 +1394,20 @@ sub tables_hashref {
         'amount',   @money_type, '', '', 
         'pkgnum', 'int', 'NULL', '', '', '', #desired pkgnum for pkg-balances
       ],
-      'primary_key' => 'creditbillnum',
-      'unique' => [],
-      'index' => [ ['crednum'], ['invnum'] ],
+      'primary_key'  => 'creditbillnum',
+      'unique'       => [],
+      'index'        => [ ['crednum'], ['invnum'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'crednum' ],
+                            table      => 'cust_credit',
+                          },
+                          { columns    => [ 'invnum' ],
+                            table      => 'cust_bill',
+                          },
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                        ],
     },
 
     'cust_credit_bill_pkg' => {
@@ -1075,13 +1422,27 @@ sub tables_hashref {
         'sdate',   @date_type, '', '', 
         'edate',   @date_type, '', '', 
       ],
-      'primary_key' => 'creditbillpkgnum',
-      'unique'      => [],
-      'index'       => [ [ 'creditbillnum' ],
-                         [ 'billpkgnum' ], 
-                         [ 'billpkgtaxlocationnum' ],
-                         [ 'billpkgtaxratelocationnum' ],
-                       ],
+      'primary_key'  => 'creditbillpkgnum',
+      'unique'       => [],
+      'index'        => [ [ 'creditbillnum' ],
+                          [ 'billpkgnum' ], 
+                          [ 'billpkgtaxlocationnum' ],
+                          [ 'billpkgtaxratelocationnum' ],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'creditbillnum' ],
+                            table      => 'cust_credit_bill',
+                          },
+                          { columns    => [ 'billpkgnum' ],
+                            table      => 'cust_bill_pkg',
+                          },
+                          { columns    => [ 'billpkgtaxlocationnum' ],
+                            table      => 'cust_bill_pkg_tax_location',
+                          },
+                          { columns    => [ 'billpkgtaxratelocationnum' ],
+                            table      => 'cust_bill_pkg_tax_rate_location',
+                          },
+                        ],
     },
 
     'cust_main' => {
@@ -1186,16 +1547,45 @@ sub tables_hashref {
         'bill_locationnum', 'int', 'NULL', '', '', '',
         'ship_locationnum', 'int', 'NULL', '', '', '',
       ],
-      'primary_key' => 'custnum',
-      'unique' => [ [ 'agentnum', 'agent_custid' ] ],
-      #'index' => [ ['last'], ['company'] ],
-      'index' => [
-                   [ 'agentnum' ], [ 'refnum' ], [ 'classnum' ], [ 'usernum' ],
-                   [ 'custbatch' ],
-                   [ 'referral_custnum' ],
-                   [ 'payby' ], [ 'paydate' ],
-                   [ 'archived' ],
-                 ],
+      'primary_key'  => 'custnum',
+      'unique'       => [ [ 'agentnum', 'agent_custid' ] ],
+      #'index'        => [ ['last'], ['company'] ],
+      'index'        => [
+                          ['agentnum'], ['refnum'], ['classnum'], ['usernum'],
+                          [ 'custbatch' ],
+                          [ 'referral_custnum' ],
+                          [ 'payby' ], [ 'paydate' ],
+                          [ 'archived' ],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                          { columns    => [ 'salesnum' ],
+                            table      => 'sales',
+                          },
+                          { columns    => [ 'refnum' ],
+                            table      => 'part_referral',
+                          },
+                          { columns    => [ 'classnum' ],
+                            table      => 'cust_class',
+                          },
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                          { columns    => [ 'referral_custnum' ],
+                            table      => 'cust_main',
+                            references => [ 'custnum' ],
+                          },
+                          { columns    => [ 'bill_locationnum' ],
+                            table      => 'cust_location',
+                            references => [ 'locationnum' ],
+                          },
+                          { columns    => [ 'ship_locationnum' ],
+                            table      => 'cust_location',
+                            references => [ 'locationnum' ],
+                          },
+                        ],
     },
 
     'cust_payby' => {
@@ -1218,9 +1608,17 @@ sub tables_hashref {
         'payip',       'varchar', 'NULL',        15, '', '', 
         'locationnum',     'int', 'NULL',        '', '', '',
       ],
-      'primary_key' => 'custpaybynum',
-      'unique'      => [],
-      'index'       => [ [ 'custnum' ] ],
+      'primary_key'  => 'custpaybynum',
+      'unique'       => [],
+      'index'        => [ [ 'custnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'locationnum' ],
+                            table      => 'cust_location',
+                          },
+                        ],
     },
 
     'cust_recon' => {  # (some sort of not-well understood thing for OnPac)
@@ -1275,11 +1673,25 @@ sub tables_hashref {
         'comment',   'varchar', 'NULL',     255, '', '', 
         'disabled',     'char', 'NULL',       1, '', '', 
       ],
-      'primary_key' => 'contactnum',
-      'unique'      => [],
-      'index'       => [ [ 'prospectnum' ], [ 'custnum' ], [ 'locationnum' ],
-                         [ 'last' ], [ 'first' ],
-                       ],
+      'primary_key'  => 'contactnum',
+      'unique'       => [],
+      'index'        => [ [ 'prospectnum' ], [ 'custnum' ], [ 'locationnum' ],
+                          [ 'last' ], [ 'first' ],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'prospectnum' ],
+                            table      => 'prospect_main',
+                          },
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'locationnum' ],
+                            table      => 'cust_location',
+                          },
+                          { columns    => [ 'classnum' ],
+                            table      => 'contact_class',
+                          },
+                        ],
     },
 
     'contact_phone' => {
@@ -1292,9 +1704,17 @@ sub tables_hashref {
         'extension',      'varchar', 'NULL',  7, '', '',
         #?#'comment',        'varchar',     '', $char_d, '', '', 
       ],
-      'primary_key' => 'contactphonenum',
-      'unique'      => [],
-      'index'       => [],
+      'primary_key'  => 'contactphonenum',
+      'unique'       => [],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'contactnum' ],
+                            table      => 'contact',
+                          },
+                          { columns    => [ 'phonetypenum' ],
+                            table      => 'phone_type',
+                          },
+                        ],
     },
 
     'phone_type' => {
@@ -1314,9 +1734,14 @@ sub tables_hashref {
         'contactnum',         'int', '',      '', '', '',
         'emailaddress',   'varchar', '', $char_d, '', '',
       ],
-      'primary_key' => 'contactemailnum',
-      'unique'      => [ [ 'contactnum', 'emailaddress' ], ],
-      'index'       => [],
+      'primary_key'  => 'contactemailnum',
+      'unique'       => [ [ 'contactnum', 'emailaddress' ], ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'contactnum' ],
+                            table      => 'contact',
+                          },
+                        ],
     },
 
     'prospect_main' => {
@@ -1328,9 +1753,17 @@ sub tables_hashref {
         'disabled',       'char', 'NULL',       1, '', '', 
         'custnum',         'int', 'NULL',      '', '', '',
       ],
-      'primary_key' => 'prospectnum',
-      'unique'      => [],
-      'index'       => [ [ 'company' ], [ 'agentnum' ], [ 'disabled' ] ],
+      'primary_key'  => 'prospectnum',
+      'unique'       => [],
+      'index'        => [ [ 'company' ], [ 'agentnum' ], [ 'disabled' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                        ],
     },
 
     'quotation' => {
@@ -1345,9 +1778,20 @@ sub tables_hashref {
         #'total',      @money_type,       '', '', 
         #'quotation_term', 'varchar', 'NULL', $char_d, '', '',
       ],
-      'primary_key' => 'quotationnum',
-      'unique' => [],
-      'index' => [ [ 'prospectnum' ], ['custnum'], ],
+      'primary_key'  => 'quotationnum',
+      'unique'       => [],
+      'index'        => [ [ 'prospectnum' ], ['custnum'], ],
+      'foreign_keys' => [
+                          { columns    => [ 'prospectnum' ],
+                            table      => 'prospect_main',
+                          },
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                        ],
     },
 
     'quotation_pkg' => {
@@ -1362,9 +1806,20 @@ sub tables_hashref {
         'quantity',             'int', 'NULL', '', '', '',
         'waive_setup',         'char', 'NULL',  1, '', '', 
       ],
-      'primary_key' => 'quotationpkgnum',
-      'unique' => [],
-      'index' => [ ['pkgpart'], ],
+      'primary_key'  => 'quotationpkgnum',
+      'unique'       => [],
+      'index'        => [ ['pkgpart'], ],
+      'foreign_keys' => [
+                          { columns    => [ 'quotationnum' ],
+                            table      => 'quotation',
+                          },
+                          { columns    => [ 'pkgpart' ],
+                            table      => 'part_pkg',
+                          },
+                          { columns    => [ 'locationnum' ],
+                            table      => 'cust_location',
+                          },
+                        ],
     },
 
     'quotation_pkg_discount' => {
@@ -1374,9 +1829,17 @@ sub tables_hashref {
         'discountnum',                'int', '', '', '', '',
         #'end_date',              @date_type,         '', '',
       ],
-      'primary_key' => 'quotationpkgdiscountnum',
-      'unique' => [],
-      'index'  => [ [ 'quotationpkgnum' ], ], #[ 'discountnum' ] ],
+      'primary_key'  => 'quotationpkgdiscountnum',
+      'unique'       => [],
+      'index'        => [ [ 'quotationpkgnum' ], ], #[ 'discountnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'quotationpkgnum' ],
+                            table      => 'quotation_pkg',
+                          },
+                          { columns    => [ 'discountnum' ],
+                            table      => 'discount',
+                          },
+                        ],
     },
 
     'cust_location' => { #'location' now that its prospects too, but...
@@ -1404,12 +1867,20 @@ sub tables_hashref {
         'location_kind',      'char', 'NULL',       1, '', '',
         'disabled',           'char', 'NULL',       1, '', '', 
       ],
-      'primary_key' => 'locationnum',
-      'unique'      => [],
-      'index'       => [ [ 'prospectnum' ], [ 'custnum' ],
-                         [ 'county' ], [ 'state' ], [ 'country' ], [ 'zip' ],
-                         [ 'city' ], [ 'district' ]
-                       ],
+      'primary_key'  => 'locationnum',
+      'unique'       => [],
+      'index'        => [ [ 'prospectnum' ], [ 'custnum' ],
+                          [ 'county' ], [ 'state' ], [ 'country' ], [ 'zip' ],
+                          [ 'city' ], [ 'district' ]
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'prospectnum' ],
+                            table      => 'prospect_main',
+                          },
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                        ],
     },
 
     'cust_main_invoice' => {
@@ -1418,9 +1889,14 @@ sub tables_hashref {
         'custnum',  'int',  '',     '', '', '', 
         'dest',     'varchar', '',  $char_d, '', '', 
       ],
-      'primary_key' => 'destnum',
-      'unique' => [],
-      'index' => [ ['custnum'], ],
+      'primary_key'  => 'destnum',
+      'unique'       => [],
+      'index'        => [ ['custnum'], ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                        ],
     },
 
     'cust_main_note' => {
@@ -1433,11 +1909,22 @@ sub tables_hashref {
         'usernum',   'int', 'NULL', '', '', '',
         'comments', 'text', 'NULL', '', '', '', 
       ],
-      'primary_key' => 'notenum',
-      'unique' => [],
-      'index' => [ [ 'custnum' ], [ '_date' ], [ 'usernum' ], ],
+      'primary_key'  => 'notenum',
+      'unique'       => [],
+      'index'        => [ [ 'custnum' ], [ '_date' ], [ 'usernum' ], ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'classnum' ],
+                            table      => 'cust_note_class',
+                          },
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                        ],
     },
-    
+
     'cust_note_class' => {
       'columns' => [
         'classnum',    'serial',   '',      '', '', '', 
@@ -1469,9 +1956,14 @@ sub tables_hashref {
         'tax',            'char', 'NULL',       1, '', '', 
         'disabled',       'char', 'NULL',       1, '', '', 
       ],
-      'primary_key' => 'classnum',
-      'unique' => [],
-      'index' => [ ['disabled'] ],
+      'primary_key'  => 'classnum',
+      'unique'       => [],
+      'index'        => [ ['disabled'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'categorynum' ],
+                            table      => 'cust_category',
+                          },
+                        ],
     },
  
     'cust_tag' => {
@@ -1480,9 +1972,17 @@ sub tables_hashref {
         'custnum',       'int', '', '', '', '',
         'tagnum',        'int', '', '', '', '',
       ],
-      'primary_key' => 'custtagnum',
-      'unique'      => [ [ 'custnum', 'tagnum' ] ],
-      'index'       => [ [ 'custnum' ] ],
+      'primary_key'  => 'custtagnum',
+      'unique'       => [ [ 'custnum', 'tagnum' ] ],
+      'index'        => [ [ 'custnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'tagnum' ],
+                            table      => 'part_tag',
+                          },
+                        ],
     },
 
     'part_tag' => {
@@ -1507,9 +2007,14 @@ sub tables_hashref {
         'exempt_number', 'varchar', 'NULL', $char_d, '', '',
         #start/end dates?  for reporting?
       ],
-      'primary_key' => 'exemptionnum',
-      'unique'      => [],
-      'index'       => [ [ 'custnum' ] ],
+      'primary_key'  => 'exemptionnum',
+      'unique'       => [],
+      'index'        => [ [ 'custnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                        ],
     },
 
     'cust_tax_adjustment' => {
@@ -1523,9 +2028,17 @@ sub tables_hashref {
         'billpkgnum',       'int', 'NULL',      '', '', '',
         #more?  no cust_bill_pkg_tax_location?
       ],
-      'primary_key' => 'adjustmentnum',
-      'unique'      => [],
-      'index'       => [ [ 'custnum' ], [ 'billpkgnum' ] ],
+      'primary_key'  => 'adjustmentnum',
+      'unique'       => [],
+      'index'        => [ [ 'custnum' ], [ 'billpkgnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'billpkgnum' ],
+                            table      => 'cust_bill_pkg',
+                          },
+                        ],
     },
 
     'cust_main_county' => { #district+city+county+state+country are checked 
@@ -1587,9 +2100,14 @@ sub tables_hashref {
         'manual',      'char', 'NULL', 1, '', '',  # Y = manually edited
         'disabled',    'char', 'NULL', 1, '', '',  # Y = tax disabled
       ],
-      'primary_key' => 'taxnum',
-      'unique' => [],
-      'index' => [ ['taxclassnum'], ['data_vendor', 'geocode'] ],
+      'primary_key'  => 'taxnum',
+      'unique'       => [],
+      'index'        => [ ['taxclassnum'], ['data_vendor', 'geocode'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'taxclassnum' ],
+                            table      => 'tax_class',
+                          },
+                        ],
     },
 
     'tax_rate_location' => { 
@@ -1666,9 +2184,29 @@ sub tables_hashref {
         'discount_term','int',     'NULL',  '', '', '',
         'failure_status','varchar','NULL',  16, '', '',
       ],
-      'primary_key' => 'paypendingnum',
-      'unique'      => [ [ 'payunique' ] ],
-      'index'       => [ [ 'custnum' ], [ 'status' ], ],
+      'primary_key'  => 'paypendingnum',
+      'unique'       => [ [ 'payunique' ] ],
+      'index'        => [ [ 'custnum' ], [ 'status' ], ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                          { columns    => [ 'gatewaynum' ],
+                            table      => 'payment_gateway',
+                          },
+                          { columns    => [ 'paynum' ],
+                            table      => 'cust_pay',
+                          },
+                          { columns    => [ 'jobnum' ],
+                            table      => 'queue',
+                          },
+                          { columns    => [ 'invnum' ],
+                            table      => 'cust_bill',
+                          },
+                        ],
     },
 
     'cust_pay' => {
@@ -1703,9 +2241,28 @@ sub tables_hashref {
         'auth',        'varchar', 'NULL',      16, '', '', # CC auth number
         'order_number','varchar', 'NULL', $char_d, '', '', # transaction number
       ],
-      'primary_key' => 'paynum',
+      'primary_key'  => 'paynum',
       #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it# 'unique' => [ [ 'payunique' ] ],
-      'index' => [ [ 'custnum' ], [ 'paybatch' ], [ 'payby' ], [ '_date' ], [ 'usernum' ] ],
+      'index'        => [ ['custnum'], ['paybatch'], ['payby'], ['_date'],
+                          ['usernum'],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                          { columns    => [ 'batchnum' ],
+                            table      => 'pay_batch',
+                          },
+                          { columns    => [ 'gatewaynum' ],
+                            table      => 'payment_gateway',
+                          },
+                        ],
     },
 
     'cust_pay_void' => {
@@ -1743,9 +2300,30 @@ sub tables_hashref {
         'reason',      'varchar', 'NULL', $char_d, '', '', 
         'void_usernum',    'int', 'NULL',      '', '', '',
       ],
-      'primary_key' => 'paynum',
-      'unique' => [],
-      'index' => [ [ 'custnum' ], [ 'usernum' ], [ 'void_usernum' ] ],
+      'primary_key'  => 'paynum',
+      'unique'       => [],
+      'index'        => [ ['custnum'], ['usernum'], ['void_usernum'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                          { columns    => [ 'batchnum' ],
+                            table      => 'pay_batch',
+                          },
+                          { columns    => [ 'gatewaynum' ],
+                            table      => 'payment_gateway',
+                          },
+                          { columns    => [ 'void_usernum' ],
+                            table      => 'access_user',
+                            references => [ 'usernum' ],
+                          },
+                        ],
     },
 
     'cust_bill_pay' => {
@@ -1757,9 +2335,20 @@ sub tables_hashref {
         '_date',   @date_type, '', '', 
         'pkgnum', 'int', 'NULL', '', '', '', #desired pkgnum for pkg-balances
       ],
-      'primary_key' => 'billpaynum',
-      'unique' => [],
-      'index' => [ [ 'paynum' ], [ 'invnum' ] ],
+      'primary_key'  => 'billpaynum',
+      'unique'       => [],
+      'index'        => [ [ 'paynum' ], [ 'invnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'invnum' ],
+                            table      => 'cust_bill',
+                          },
+                          { columns    => [ 'paynum' ],
+                            table      => 'cust_pay',
+                          },
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                        ],
     },
 
     'cust_bill_pay_batch' => {
@@ -1770,9 +2359,17 @@ sub tables_hashref {
         'amount',  @money_type, '', '', 
         '_date',   @date_type, '', '', 
       ],
-      'primary_key' => 'billpaynum',
-      'unique' => [],
-      'index' => [ [ 'paybatchnum' ], [ 'invnum' ] ],
+      'primary_key'  => 'billpaynum',
+      'unique'       => [],
+      'index'        => [ [ 'paybatchnum' ], [ 'invnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'invnum' ],
+                            table      => 'cust_bill',
+                          },
+                          { columns    => [ 'paybatchnum' ],
+                            table      => 'cust_pay_batch',
+                          },
+                        ],
     },
 
     'cust_bill_pay_pkg' => {
@@ -1787,9 +2384,23 @@ sub tables_hashref {
        'sdate',   @date_type, '', '', 
         'edate',   @date_type, '', '', 
       ],
-      'primary_key' => 'billpaypkgnum',
-      'unique'      => [],
-      'index'       => [ [ 'billpaynum' ], [ 'billpkgnum' ], ],
+      'primary_key'  => 'billpaypkgnum',
+      'unique'       => [],
+      'index'        => [ [ 'billpaynum' ], [ 'billpkgnum' ], ],
+      'foreign_keys' => [
+                          { columns    => [ 'billpaynum' ],
+                            table      => 'cust_bill_pay_batch',
+                          },
+                          { columns    => [ 'billpkgnum' ],
+                            table      => 'cust_bill_pkg',
+                          },
+                          { columns    => [ 'billpkgtaxlocationnum' ],
+                            table      => 'cust_bill_pkg_tax_location',
+                          },
+                          { columns    => [ 'billpkgtaxratelocationnum' ],
+                            table      => 'cust_bill_pkg_tax_rate_location',
+                          },
+                        ],
     },
 
     'pay_batch' => { #batches of payments to an external processor
@@ -1802,9 +2413,14 @@ sub tables_hashref {
         'upload',         @date_type,     '', '', 
         'title',   'varchar', 'NULL',255, '', '',
       ],
-      'primary_key' => 'batchnum',
-      'unique' => [],
-      'index' => [],
+      'primary_key'  => 'batchnum',
+      'unique'       => [],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                        ],
     },
 
     'cust_pay_batch' => { #list of customers in current CARD/CHEK batch
@@ -1832,9 +2448,20 @@ sub tables_hashref {
         'failure_status','varchar', 'NULL',      16, '', '',
         'error_message', 'varchar', 'NULL', $char_d, '', '',
       ],
-      'primary_key' => 'paybatchnum',
-      'unique' => [],
-      'index' => [ ['batchnum'], ['invnum'], ['custnum'] ],
+      'primary_key'  => 'paybatchnum',
+      'unique'       => [],
+      'index'        => [ ['batchnum'], ['invnum'], ['custnum'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'batchnum' ],
+                            table      => 'pay_batch',
+                          },
+                          { columns    => [ 'invnum' ],
+                            table      => 'cust_bill',
+                          },
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                        ],
     },
 
     'fcc477map' => {
@@ -1888,15 +2515,67 @@ sub tables_hashref {
         'setup_show_zero',    'char', 'NULL',  1, '', '',
         'change_to_pkgnum',    'int', 'NULL', '', '', '',
       ],
-      'primary_key' => 'pkgnum',
-      'unique' => [],
-      'index' => [ ['custnum'], ['pkgpart'], [ 'pkgbatch' ], [ 'locationnum' ],
-                   [ 'usernum' ], [ 'agent_pkgid' ],
-                   ['order_date'], [ 'start_date' ], ['setup'], ['bill'],
-                   ['last_bill'], ['susp'], ['adjourn'], ['cancel'],
-                   ['expire'], ['contract_end'], ['change_date'], ['no_auto'],
-                 ],
-    },
+      'primary_key'  => 'pkgnum',
+      'unique'       => [],
+      'index'        => [ ['custnum'], ['pkgpart'], ['pkgbatch'],
+                          ['locationnum'], ['usernum'], ['agent_pkgid'],
+                          ['order_date'], [ 'start_date' ], ['setup'], ['bill'],
+                          ['last_bill'], ['susp'], ['adjourn'], ['cancel'],
+                          ['expire'], ['contract_end'], ['change_date'],
+                          ['no_auto'],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'pkgpart' ],
+                            table      => 'part_pkg',
+                          },
+                          { columns    => [ 'contactnum' ],
+                            table      => 'contact',
+                          },
+                          { columns    => [ 'locationnum' ],
+                            table      => 'cust_location',
+                          },
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                          { columns    => [ 'salesnum' ],
+                            table      => 'sales',
+                          },
+                          { columns    => [ 'uncancel_pkgnum' ],
+                            table      => 'cust_pkg',
+                            references => [ 'pkgnum' ],
+                          },
+                          { columns    => [ 'change_pkgnum' ],
+                            table      => 'cust_pkg',
+                            references => [ 'pkgnum' ],
+                          },
+                          { columns    => [ 'change_pkgpart' ],
+                            table      => 'part_pkg',
+                            references => [ 'pkgpart' ],
+                          },
+                          { columns    => [ 'change_locationnum' ],
+                            table      => 'cust_location',
+                            references => [ 'locationnum' ],
+                          },
+                          { columns    => [ 'change_custnum' ],
+                            table      => 'cust_main',
+                            references => [ 'custnum' ],
+                          },
+                          { columns    => [ 'main_pkgnum' ],
+                            table      => 'cust_pkg',
+                            references => [ 'pkgnum' ],
+                          },
+                          { columns    => [ 'pkglinknum' ],
+                            table      => 'part_pkg_link',
+                          },
+                          { columns    => [ 'change_to_pkgnum' ],
+                            table      => 'cust_pkg',
+                            references => [ 'pkgnum' ],
+                          },
+                        ],
+   },
 
     'cust_pkg_option' => {
       'columns' => [
@@ -1905,9 +2584,14 @@ sub tables_hashref {
         'optionname', 'varchar', '', $char_d, '', '', 
         'optionvalue', 'text', 'NULL', '', '', '', 
       ],
-      'primary_key' => 'optionnum',
-      'unique'      => [],
-      'index'       => [ [ 'pkgnum' ], [ 'optionname' ] ],
+      'primary_key'  => 'optionnum',
+      'unique'       => [],
+      'index'        => [ [ 'pkgnum' ], [ 'optionname' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                        ],
     },
 
     'cust_pkg_detail' => {
@@ -1918,9 +2602,14 @@ sub tables_hashref {
         'detailtype',     'char', '',       1, '', '', # "I"nvoice or "C"omment
         'weight',          'int', '',      '', '', '',
       ],
-      'primary_key' => 'pkgdetailnum',
-      'unique' => [],
-      'index'  => [ [ 'pkgnum', 'detailtype' ] ],
+      'primary_key'  => 'pkgdetailnum',
+      'unique'       => [],
+      'index'        => [ [ 'pkgnum', 'detailtype' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                        ],
     },
 
     'cust_pkg_reason' => {
@@ -1933,9 +2622,20 @@ sub tables_hashref {
         'usernum',   'int', 'NULL', '', '', '',
         'date',     @date_type, '', '', 
       ],
-      'primary_key' => 'num',
-      'unique' => [],
-      'index' => [ [ 'pkgnum' ], [ 'reasonnum' ], ['action'], [ 'usernum' ], ],
+      'primary_key'  => 'num',
+      'unique'       => [],
+      'index'        => [ ['pkgnum'], ['reasonnum'], ['action'], ['usernum'], ],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                          { columns    => [ 'reasonnum' ],
+                            table      => 'reason',
+                          },
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                        ],
     },
 
     'cust_pkg_discount' => {
@@ -1949,9 +2649,20 @@ sub tables_hashref {
         'usernum',           'int', 'NULL',    '', '', '',
         'disabled',         'char', 'NULL',     1, '', '', 
       ],
-      'primary_key' => 'pkgdiscountnum',
-      'unique' => [],
-      'index'  => [ [ 'pkgnum' ], [ 'discountnum' ], [ 'usernum' ], ],
+      'primary_key'  => 'pkgdiscountnum',
+      'unique'       => [],
+      'index'        => [ [ 'pkgnum' ], [ 'discountnum' ], [ 'usernum' ], ],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                          { columns    => [ 'discountnum' ],
+                            table      => 'discount',
+                          },
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                        ],
     },
 
     'cust_pkg_usage' => {
@@ -1961,9 +2672,17 @@ sub tables_hashref {
         'minutes',        'int', '', '', '', '',
         'pkgusagepart',   'int', '', '', '', '',
       ],
-      'primary_key' => 'pkgusagenum',
-      'unique' => [],
-      'index'  => [ [ 'pkgnum' ], [ 'pkgusagepart' ] ],
+      'primary_key'  => 'pkgusagenum',
+      'unique'       => [],
+      'index'        => [ [ 'pkgnum' ], [ 'pkgusagepart' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                          { columns    => [ 'pkgusagepart' ],
+                            table      => 'part_pkg_usage',
+                          },
+                        ],
     },
 
     'cdr_cust_pkg_usage' => {
@@ -1973,9 +2692,17 @@ sub tables_hashref {
         'pkgusagenum', 'int',       '', '', '', '',
         'minutes',     'int',       '', '', '', '',
       ],
-      'primary_key' => 'cdrusagenum',
-      'unique' => [],
-      'index'  => [ [ 'pkgusagenum' ], [ 'acctid' ] ],
+      'primary_key'  => 'cdrusagenum',
+      'unique'       => [],
+      'index'        => [ [ 'pkgusagenum' ], [ 'acctid' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'acctid' ],
+                            table      => 'cdr',
+                          },
+                          { columns    => [ 'pkgusagenum' ],
+                            table      => 'cust_pkg_usage',
+                          },
+                        ],
     },
 
     'cust_bill_pkg_discount' => {
@@ -1986,9 +2713,17 @@ sub tables_hashref {
         'amount',          @money_type,                '', '', 
         'months',            'decimal', 'NULL', '7,4', '', '',
       ],
-      'primary_key' => 'billpkgdiscountnum',
-      'unique' => [],
-      'index' => [ [ 'billpkgnum' ], [ 'pkgdiscountnum' ] ],
+      'primary_key'  => 'billpkgdiscountnum',
+      'unique'       => [],
+      'index'        => [ [ 'billpkgnum' ], [ 'pkgdiscountnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'billpkgnum' ],
+                            table      => 'cust_bill_pkg',
+                          },
+                          { columns    => [ 'pkgdiscountnum' ],
+                            table      => 'cust_pkg_discount',
+                          },
+                        ],
     },
 
     'cust_bill_pkg_discount_void' => {
@@ -1999,15 +2734,24 @@ sub tables_hashref {
         'amount',          @money_type,                '', '', 
         'months',            'decimal', 'NULL', '7,4', '', '',
       ],
-      'primary_key' => 'billpkgdiscountnum',
-      'unique' => [],
-      'index' => [ [ 'billpkgnum' ], [ 'pkgdiscountnum' ] ],
+      'primary_key'  => 'billpkgdiscountnum',
+      'unique'       => [],
+      'index'        => [ [ 'billpkgnum' ], [ 'pkgdiscountnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'billpkgnum' ],
+                            table      => 'cust_bill_pkg_void',
+                          },
+                          { columns    => [ 'pkgdiscountnum' ],
+                            table      => 'cust_pkg_discount',
+                          },
+                        ],
     },
 
     'discount' => {
       'columns' => [
         'discountnum', 'serial',     '',      '', '', '',
         #'agentnum',       'int', 'NULL',      '', '', '', 
+        'classnum',       'int', 'NULL',      '', '', '',
         'name',       'varchar', 'NULL', $char_d, '', '',
         'amount',   @money_type,                  '', '', 
         'percent',    'decimal',     '',   '7,4', '', '',
@@ -2016,9 +2760,26 @@ sub tables_hashref {
         'setup',         'char', 'NULL',       1, '', '', 
         #'linked',        'char', 'NULL',       1, '', '',
       ],
-      'primary_key' => 'discountnum',
+      'primary_key'  => 'discountnum',
+      'unique'       => [],
+      'index'        => [], # [ 'agentnum' ], ],
+      'foreign_keys' => [
+                          { columns    => [ 'classnum' ],
+                            table      => 'discount_class',
+                          },
+                        ],
+    },
+
+    'discount_class' => {
+      'columns' => [
+        'classnum',    'serial',   '',      '', '', '', 
+        'classname',   'varchar',  '', $char_d, '', '', 
+        #'categorynum', 'int',  'NULL',      '', '', '', 
+        'disabled',    'char', 'NULL',       1, '', '', 
+      ],
+      'primary_key' => 'classnum',
       'unique' => [],
-      'index'  => [], # [ 'agentnum' ], ],
+      'index' => [ ['disabled'] ],
     },
 
     'cust_refund' => {
@@ -2044,9 +2805,20 @@ sub tables_hashref {
         'auth',       'varchar','NULL',16, '', '', # CC auth number
         'order_number', 'varchar','NULL',$char_d, '', '', # transaction number
       ],
-      'primary_key' => 'refundnum',
-      'unique' => [],
-      'index' => [ ['custnum'], ['_date'], [ 'usernum' ], ],
+      'primary_key'  => 'refundnum',
+      'unique'       => [],
+      'index'        => [ ['custnum'], ['_date'], [ 'usernum' ], ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                          { columns    => [ 'gatewaynum' ],
+                            table      => 'payment_gateway',
+                          },
+                        ],
     },
 
     'cust_credit_refund' => {
@@ -2057,9 +2829,17 @@ sub tables_hashref {
         'amount',  @money_type, '', '', 
         '_date',   @date_type, '', '', 
       ],
-      'primary_key' => 'creditrefundnum',
-      'unique' => [],
-      'index' => [ ['crednum'], ['refundnum'] ],
+      'primary_key'  => 'creditrefundnum',
+      'unique'       => [],
+      'index'        => [ ['crednum'], ['refundnum'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'crednum' ],
+                            table      => 'cust_credit',
+                          },
+                          { columns    => [ 'refundnum' ],
+                            table      => 'cust_refund',
+                          },
+                        ],
     },
 
 
@@ -2071,9 +2851,19 @@ sub tables_hashref {
         'agent_svcid',    'int', 'NULL', '', '', '',
         'overlimit',           @date_type,   '', '', 
       ],
-      'primary_key' => 'svcnum',
-      'unique' => [],
-      'index' => [ ['svcnum'], ['pkgnum'], ['svcpart'], [ 'agent_svcid' ] ],
+      'primary_key'  => 'svcnum',
+      'unique'       => [],
+      'index'        => [ ['svcnum'], ['pkgnum'], ['svcpart'],
+                          ['agent_svcid'],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                          { columns    => [ 'svcpart' ],
+                            table      => 'part_svc',
+                          },
+                        ],
     },
 
     'cust_svc_option' => {
@@ -2083,9 +2873,14 @@ sub tables_hashref {
         'optionname',  'varchar', '', $char_d, '', '', 
         'optionvalue', 'text', 'NULL', '', '', '', 
       ],
-      'primary_key' => 'optionnum',
-      'unique'      => [],
-      'index'       => [ [ 'svcnum' ], [ 'optionname' ] ],
+      'primary_key'  => 'optionnum',
+      'unique'       => [],
+      'index'        => [ [ 'svcnum' ], [ 'optionname' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                        ],
     },
 
     'svc_export_machine' => {
@@ -2095,9 +2890,20 @@ sub tables_hashref {
         'exportnum',              'int', '', '', '', '', 
         'machinenum',             'int', '', '', '', '',
       ],
-      'primary_key' => 'svcexportmachinenum',
-      'unique'      => [ ['svcnum', 'exportnum'] ],
-      'index'       => [],
+      'primary_key'  => 'svcexportmachinenum',
+      'unique'       => [ ['svcnum', 'exportnum'] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                          { columns    => [ 'exportnum' ],
+                            table      => 'part_export',
+                          },
+                          { columns    => [ 'machinenum' ],
+                            table      => 'part_export_machine',
+                          },
+                        ],
     },
 
     'part_export_machine' => {
@@ -2107,9 +2913,14 @@ sub tables_hashref {
         'machine',    'varchar', 'NULL', $char_d, '', '', 
         'disabled',      'char', 'NULL',       1, '', '',
       ],
-      'primary_key' => 'machinenum',
-      'unique'      => [ [ 'exportnum', 'machine' ] ],
-      'index'       => [ [ 'exportnum' ] ],
+      'primary_key'  => 'machinenum',
+      'unique'       => [ [ 'exportnum', 'machine' ] ],
+      'index'        => [ [ 'exportnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'exportnum' ],
+                            table      => 'part_export',
+                          },
+                        ],
     },
 
    'part_pkg' => {
@@ -2142,11 +2953,34 @@ sub tables_hashref {
         'family_pkgpart','int',     'NULL', '', '', '',
         'delay_start',   'int',     'NULL', '', '', '',
       ],
-      'primary_key' => 'pkgpart',
-      'unique' => [],
-      'index' => [ [ 'promo_code' ], [ 'disabled' ], [ 'classnum' ],
-                   [ 'agentnum' ], ['no_auto'],
-                 ],
+      'primary_key'  => 'pkgpart',
+      'unique'       => [],
+      'index'        => [ [ 'promo_code' ], [ 'disabled' ], [ 'classnum' ],
+                          [ 'agentnum' ], ['no_auto'],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'classnum' ],
+                            table      => 'pkg_class',
+                          },
+                          { columns    => [ 'addon_classnum' ],
+                            table      => 'pkg_class',
+                            references => [ 'classnum' ],
+                          },
+                          { columns    => [ 'taxproductnum' ],
+                            table      => 'part_pkg_taxproduct',
+                          },
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                          { columns    => [ 'successor' ],
+                            table      => 'part_pkg',
+                            references => [ 'pkgpart' ],
+                          },
+                          { columns    => [ 'family_pkgpart' ],
+                            table      => 'part_pkg',
+                            references => [ 'pkgpart' ],
+                          },
+                        ],
     },
 
     'part_pkg_msgcat' => {
@@ -2157,9 +2991,14 @@ sub tables_hashref {
         'pkg',           'varchar',     '',   $char_d, '', '', #longer/no limit?
         'comment',       'varchar', 'NULL', 2*$char_d, '', '', #longer/no limit?
       ],
-      'primary_key' => 'pkgpartmsgnum',
-      'unique'      => [ [ 'pkgpart', 'locale' ] ],
-      'index'       => [],
+      'primary_key'  => 'pkgpartmsgnum',
+      'unique'       => [ [ 'pkgpart', 'locale' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgpart' ],
+                            table      => 'part_pkg',
+                          },
+                        ],
     },
 
     'part_pkg_currency' => {
@@ -2170,9 +3009,14 @@ sub tables_hashref {
         'optionname',    'varchar', '', $char_d, '', '', 
         'optionvalue',      'text', '',      '', '', '', 
       ],
-      'primary_key' => 'pkgcurrencynum',
-      'unique'      => [ [ 'pkgpart', 'currency', 'optionname' ] ],
-      'index'       => [ ['pkgpart'] ],
+      'primary_key'  => 'pkgcurrencynum',
+      'unique'       => [ [ 'pkgpart', 'currency', 'optionname' ] ],
+      'index'        => [ ['pkgpart'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgpart' ],
+                            table      => 'part_pkg',
+                          },
+                        ],
     },
 
     'currency_exchange' => {
@@ -2195,9 +3039,19 @@ sub tables_hashref {
         'link_type',   'varchar',  '', $char_d, '', '',
         'hidden',      'char', 'NULL',       1, '', '',
       ],
-      'primary_key' => 'pkglinknum',
-      'unique' => [ [ 'src_pkgpart', 'dst_pkgpart', 'link_type', 'hidden' ] ],
-      'index'  => [ [ 'src_pkgpart' ] ],
+      'primary_key'  => 'pkglinknum',
+      'unique'       => [ ['src_pkgpart','dst_pkgpart','link_type','hidden'] ],
+      'index'        => [ [ 'src_pkgpart' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'src_pkgpart' ],
+                            table      => 'part_pkg',
+                            references => [ 'pkgpart' ]
+                          },
+                          { columns    => [ 'dst_pkgpart' ],
+                            table      => 'part_pkg',
+                            references => [ 'pkgpart' ]
+                          },
+                        ],
     },
     # XXX somewhat borked unique: we don't really want a hidden and unhidden
     # it turns out we'd prefer to use svc, bill, and invisibill (or something)
@@ -2208,9 +3062,17 @@ sub tables_hashref {
         'pkgpart',        'int',      '',      '', '', '',
         'discountnum',    'int',      '',      '', '', '', 
       ],
-      'primary_key' => 'pkgdiscountnum',
-      'unique' => [ [ 'pkgpart', 'discountnum' ] ],
-      'index'  => [],
+      'primary_key'  => 'pkgdiscountnum',
+      'unique'       => [ [ 'pkgpart', 'discountnum' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgpart' ],
+                            table      => 'part_pkg',
+                          },
+                          { columns    => [ 'discountnum' ],
+                            table      => 'discount',
+                          },
+                        ],
     },
 
     'part_pkg_taxclass' => {
@@ -2253,9 +3115,21 @@ sub tables_hashref {
         'effdate',          @date_type, '', '', 
         'taxable',          'char',    'NULL', 1,       '', '', 
       ],
-      'primary_key' => 'pkgtaxratenum',
-      'unique' => [],
-      'index' => [ [ 'data_vendor', 'geocode', 'taxproductnum' ] ],
+      'primary_key'  => 'pkgtaxratenum',
+      'unique'       => [],
+      'index'        => [ [ 'data_vendor', 'geocode', 'taxproductnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'taxproductnum' ],
+                            table      => 'part_pkg_taxproduct',
+                          },
+                          { columns    => [ 'taxclassnumtaxed' ],
+                            table      => 'tax_class',
+                            references => [ 'taxclassnum' ],
+                          },
+                          { columns    => [ 'taxclassnum' ],
+                            table      => 'tax_class',
+                          },
+                        ],
     },
 
     'part_pkg_taxoverride' => { 
@@ -2265,9 +3139,17 @@ sub tables_hashref {
         'taxclassnum',       'int', '', '', '', '',
         'usage_class',    'varchar', 'NULL', $char_d, '', '', 
       ],
-      'primary_key' => 'taxoverridenum',
-      'unique' => [],
-      'index' => [ [ 'pkgpart' ], [ 'taxclassnum' ] ],
+      'primary_key'  => 'taxoverridenum',
+      'unique'       => [],
+      'index'        => [ [ 'pkgpart' ], [ 'taxclassnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgpart' ],
+                            table      => 'part_pkg',
+                          },
+                          { columns    => [ 'taxclassnum' ],
+                            table      => 'tax_class',
+                          },
+                        ],
     },
 
 #    'part_title' => {
@@ -2290,9 +3172,17 @@ sub tables_hashref {
         'hidden',        'char', 'NULL', 1, '', '',
         'bulk_skip',     'char', 'NULL', 1, '', '',
       ],
-      'primary_key' => 'pkgsvcnum',
-      'unique'      => [ ['pkgpart', 'svcpart'] ],
-      'index'       => [ ['pkgpart'], ['quantity'] ],
+      'primary_key'  => 'pkgsvcnum',
+      'unique'       => [ ['pkgpart', 'svcpart'] ],
+      'index'        => [ ['pkgpart'], ['quantity'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgpart' ],
+                            table      => 'part_pkg',
+                          },
+                          { columns    => [ 'svcpart' ],
+                            table      => 'part_svc',
+                          },
+                        ],
     },
 
     'part_referral' => {
@@ -2302,9 +3192,14 @@ sub tables_hashref {
         'disabled', 'char',   'NULL',         1, '', '', 
         'agentnum', 'int',    'NULL',        '', '', '', 
       ],
-      'primary_key' => 'refnum',
-      'unique' => [],
-      'index' => [ ['disabled'], ['agentnum'], ],
+      'primary_key'  => 'refnum',
+      'unique'       => [],
+      'index'        => [ ['disabled'], ['agentnum'], ],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                        ],
     },
 
     'part_svc' => {
@@ -2319,9 +3214,14 @@ sub tables_hashref {
         'restrict_edit_password','char', 'NULL',         1, '', '',
         'has_router',            'char', 'NULL',         1, '', '',
 ],
-      'primary_key' => 'svcpart',
-      'unique' => [],
-      'index' => [ [ 'disabled' ] ],
+      'primary_key'  => 'svcpart',
+      'unique'       => [],
+      'index'        => [ [ 'disabled' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'classnum' ],
+                            table      => 'part_svc_class',
+                          },
+                        ],
     },
 
     'part_svc_column' => {
@@ -2333,9 +3233,14 @@ sub tables_hashref {
         'columnvalue', 'varchar', 'NULL',     512, '', '', 
         'columnflag',  'char',    'NULL',       1, '', '', 
       ],
-      'primary_key' => 'columnnum',
-      'unique' => [ [ 'svcpart', 'columnname' ] ],
-      'index' => [ [ 'svcpart' ] ],
+      'primary_key'  => 'columnnum',
+      'unique'       => [ [ 'svcpart', 'columnname' ] ],
+      'index'        => [ [ 'svcpart' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcpart' ],
+                            table      => 'part_svc',
+                          },
+                        ],
     },
 
     'part_svc_class' => {
@@ -2373,11 +3278,16 @@ sub tables_hashref {
         'npa',       'char',    '',     3, '', '', 
         'nxx',       'char',    '',     3, '', '', 
       ],
-      'primary_key' => 'localnum',
-      'unique' => [],
-      'index' => [ [ 'npa', 'nxx' ], [ 'popnum' ] ],
+      'primary_key'  => 'localnum',
+      'unique'       => [],
+      'index'        => [ [ 'npa', 'nxx' ], [ 'popnum' ] ],
+      'foreign_keys' => [
+                         { columns    => [ 'popnum' ],
+                           table      => 'svc_acct_pop',
+                         },
+                       ],
     },
-    
+
     'qual' => {
       'columns' => [
         'qualnum',  'serial',     '',     '', '', '', 
@@ -2389,12 +3299,27 @@ sub tables_hashref {
         'vendor_qual_id',      'varchar', 'NULL', $char_d, '', '', 
         'status',      'char', '', 1, '', '', 
       ],
-      'primary_key' => 'qualnum',
-      'unique' => [],
-      'index' => [ [ 'locationnum' ], ['custnum'], ['prospectnum'],
-                   ['phonenum'], ['vendor_qual_id'] ],
+      'primary_key'  => 'qualnum',
+      'unique'       => [],
+      'index'        => [ ['locationnum'], ['custnum'], ['prospectnum'],
+                         ['phonenum'], ['vendor_qual_id'],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'prospectnum' ],
+                            table      => 'prospect_main',
+                          },
+                          { columns    => [ 'locationnum' ],
+                            table      => 'cust_location',
+                          },
+                          { columns    => [ 'exportnum' ],
+                            table      => 'part_export',
+                          },
+                        ],
     },
-    
+
     'qual_option' => {
       'columns' => [
         'optionnum', 'serial', '', '', '', '', 
@@ -2402,9 +3327,14 @@ sub tables_hashref {
         'optionname', 'varchar', '', $char_d, '', '', 
         'optionvalue', 'text', 'NULL', '', '', '', 
       ],
-      'primary_key' => 'optionnum',
-      'unique' => [],
-      'index' => [],
+      'primary_key'  => 'optionnum',
+      'unique'       => [],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'qualnum' ],
+                            table      => 'qual',
+                          },
+                        ],
     },
 
     'svc_acct' => {
@@ -2468,24 +3398,62 @@ sub tables_hashref {
         #XXX RPOP settings
         #
       ],
-      'primary_key' => 'svcnum',
-      #'unique' => [ [ 'username', 'domsvc' ] ],
-      'unique' => [],
-      'index' => [ ['username'], ['domsvc'], ['pbxsvc'] ],
+      'primary_key'  => 'svcnum',
+      #'unique'       => [ [ 'username', 'domsvc' ] ],
+      'unique'       => [],
+      'index'        => [ ['username'], ['domsvc'], ['pbxsvc'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                          { columns    => [ 'popnum' ],
+                            table      => 'svc_acct_pop',
+                          },
+                          { columns    => [ 'sectornum' ],
+                            table      => 'tower_sector',
+                          },
+                          { columns    => [ 'routernum' ],
+                            table      => 'router',
+                          },
+                          { columns    => [ 'blocknum' ],
+                            table      => 'addr_block',
+                          },
+                          { columns    => [ 'domsvc' ],
+                            table      => 'svc_domain', #'cust_svc',
+                            references => [ 'svcnum' ],
+                          },
+                          { columns    => [ 'pbxsvc' ],
+                            table      => 'svc_pbx', #'cust_svc',
+                            references => [ 'svcnum' ],
+                          },
+                        ],
     },
 
     'acct_rt_transaction' => {
       'columns' => [
-        'svcrtid',   'int',    '',   '', '', '', 
+        'svcrtid',   'int',    '',   '', '', '', #why am i not a serial
         'svcnum',    'int',    '',   '', '', '', 
         'transaction_id',       'int', '',   '', '', '', 
         '_date',   @date_type, '', '',
         'seconds',   'int', '',   '', '', '', #uhhhh
         'support',   'int', '',   '', '', '',
       ],
-      'primary_key' => 'svcrtid',
-      'unique' => [],
-      'index' => [ ['svcnum', 'transaction_id'] ],
+      'primary_key'  => 'svcrtid',
+      'unique'       => [],
+      'index'        => [ ['svcnum', 'transaction_id'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'svc_acct', #'cust_svc',
+                          },
+                          # 1. RT tables aren't part of our data structure, so
+                          #     we can't make sure Queue is created already
+                          # 2. This is our internal hack for time tracking, not
+                          #     a user-facing feature
+                          #{ columns    => [ 'transaction_id' ],
+                          #  table      => 'Transaction',
+                          #  references => [ 'id' ],
+                          #},
+                        ],
     },
 
     #'svc_charge' => {
@@ -2540,11 +3508,27 @@ sub tables_hashref {
         'acct_def_cgp_prontoskinname', 'varchar', 'NULL', $char_d,  '', '',
         'acct_def_cgp_sendmdnmode',    'varchar', 'NULL', $char_d,  '', '',
       ],
-      'primary_key' => 'svcnum',
-      'unique' => [ ],
-      'index' => [ ['domain'] ],
+      'primary_key'  => 'svcnum',
+      'unique'       => [],
+      'index'        => [ ['domain'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                          { columns    => [ 'catchall' ],
+                            table      => 'svc_acct',
+                            references => [ 'svcnum' ],
+                          },
+                          { columns    => [ 'parent_svcnum' ],
+                            table      => 'cust_svc',
+                            references => [ 'svcnum' ],
+                          },
+                          { columns    => [ 'registrarnum' ],
+                            table      => 'registrar',
+                          },
+                        ],
     },
-    
+
     'svc_dsl' => {
       'columns' => [
         'svcnum',                    'int',    '',        '', '', '',
@@ -2574,9 +3558,14 @@ sub tables_hashref {
         'monitored',                'char', 'NULL',       1, '', '', 
         'last_pull',                 'int', 'NULL',      '', '', '',
       ],
-      'primary_key' => 'svcnum',
-      'unique' => [ ],
-      'index' => [ ['phonenum'], ['vendor_order_id'] ],
+      'primary_key'  => 'svcnum',
+      'unique'       => [ ],
+      'index'        => [ ['phonenum'], ['vendor_order_id'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                        ],
     },
 
     'dsl_device' => {
@@ -2587,11 +3576,16 @@ sub tables_hashref {
         'svcnum',       'int',     '', '', '', '', 
         'mac_addr', 'varchar',     '', 12, '', '', 
       ],
-      'primary_key' => 'devicenum',
-      'unique' => [ [ 'mac_addr' ], ],
-      'index'  => [ [ 'svcnum' ], ], # [ 'devicepart' ] ],
+      'primary_key'  => 'devicenum',
+      'unique'       => [ [ 'mac_addr' ], ],
+      'index'        => [ [ 'svcnum' ], ], # [ 'devicepart' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'svc_dsl',
+                          },
+                        ],
     },
-    
+
     'dsl_note' => {
       'columns' => [
         'notenum',           'serial',    '',        '', '', '',
@@ -2601,9 +3595,14 @@ sub tables_hashref {
        '_date',     'int', 'NULL',       '', '', '',
        'note',     'text', '',       '', '', '',
       ],
-      'primary_key' => 'notenum',
-      'unique' => [ ],
-      'index' => [ ['svcnum'] ],
+      'primary_key'  => 'notenum',
+      'unique'       => [],
+      'index'        => [ ['svcnum'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'svc_dsl',
+                          },
+                        ],
     },
 
     'svc_dish' => {
@@ -2613,9 +3612,14 @@ sub tables_hashref {
         'installdate', @date_type,         '', '', 
         'note',     'text',    'NULL', '', '', '',
       ],
-      'primary_key' => 'svcnum',
-      'unique' => [ ],
-      'index' => [ ],
+      'primary_key'  => 'svcnum',
+      'unique'       => [],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                        ],
     },
 
     'svc_hardware' => {
@@ -2629,9 +3633,20 @@ sub tables_hashref {
         'statusnum','int',     'NULL',      '', '', '',
         'note',     'text',    'NULL',      '', '', '',
       ],
-      'primary_key' => 'svcnum',
-      'unique' => [ ],
-      'index' => [ ],
+      'primary_key'  => 'svcnum',
+      'unique'       => [],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                          { columns    => [ 'typenum' ],
+                            table      => 'hardware_type',
+                          },
+                          { columns    => [ 'statusnum' ],
+                            table      => 'hardware_status',
+                          },
+                        ],
     },
 
     'hardware_class' => {
@@ -2651,9 +3666,14 @@ sub tables_hashref {
         'model',   'varchar',     '', $char_d, '', '',
         'revision','varchar', 'NULL', $char_d, '', '',
       ],
-      'primary_key' => 'typenum',
-      'unique' => [ [ 'classnum', 'model', 'revision' ] ],
-      'index'  => [ ],
+      'primary_key'  => 'typenum',
+      'unique'       => [ [ 'classnum', 'model', 'revision' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'classnum' ],
+                            table      => 'hardware_class',
+                          },
+                        ],
     },
 
     'hardware_status' => {
@@ -2677,9 +3697,14 @@ sub tables_hashref {
         'recdata',   'varchar', '',  255, '', '', 
         'ttl',       'int',     'NULL', '', '', '',
       ],
-      'primary_key' => 'recnum',
-      'unique'      => [],
-      'index'       => [ ['svcnum'] ],
+      'primary_key'  => 'recnum',
+      'unique'       => [],
+      'index'        => [ ['svcnum'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'svc_domain',
+                          },
+                        ],
     },
 
     'registrar' => {
@@ -2703,6 +3728,11 @@ sub tables_hashref {
       'primary_key' => 'rulenum',
       'unique'      => [ [ 'svcnum', 'name' ] ],
       'index'       => [ [ 'svcnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc', #svc_acct / svc_domain
+                          },
+                        ],
     },
 
     'cgp_rule_condition' => {
@@ -2713,9 +3743,14 @@ sub tables_hashref {
         'params',           'varchar', 'NULL',     255, '', '',
         'rulenum',              'int',     '',      '', '', '',
       ],
-      'primary_key' => 'ruleconditionnum',
-      'unique'      => [],
-      'index'       => [ [ 'rulenum' ] ],
+      'primary_key'  => 'ruleconditionnum',
+      'unique'       => [],
+      'index'        => [ [ 'rulenum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'rulenum' ],
+                            table      => 'cgp_rule',
+                          },
+                        ],
     },
 
     'cgp_rule_action' => {
@@ -2725,9 +3760,14 @@ sub tables_hashref {
         'params',        'varchar', 'NULL',     255, '', '',
         'rulenum',           'int',     '',      '', '', '',
       ],
-      'primary_key' => 'ruleactionnum',
-      'unique'      => [],
-      'index'       => [ [ 'rulenum' ] ],
+      'primary_key'  => 'ruleactionnum',
+      'unique'       => [],
+      'index'        => [ [ 'rulenum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'rulenum' ],
+                            table      => 'cgp_rule',
+                          },
+                        ],
    },
 
     'svc_forward' => {
@@ -2738,9 +3778,22 @@ sub tables_hashref {
         'dstsvc',   'int',        'NULL',   '', '', '', 
         'dst',      'varchar',    'NULL',  255, '', '', 
       ],
-      'primary_key' => 'svcnum',
-      'unique'      => [],
-      'index'       => [ ['srcsvc'], ['dstsvc'] ],
+      'primary_key'  => 'svcnum',
+      'unique'       => [],
+      'index'        => [ ['srcsvc'], ['dstsvc'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                          { columns    => [ 'srcsvc' ],
+                            table      => 'svc_acct',
+                            references => [ 'svcnum' ]
+                          },
+                          { columns    => [ 'dstsvc' ],
+                            table      => 'svc_acct',
+                            references => [ 'svcnum' ]
+                          },
+                        ],
     },
 
     'svc_www' => {
@@ -2753,6 +3806,18 @@ sub tables_hashref {
       'primary_key' => 'svcnum',
       'unique'      => [],
       'index'       => [],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                          { columns    => [ 'recnum' ],
+                            table      => 'domain_record',
+                          },
+                          { columns    => [ 'usersvc' ],
+                            table      => 'svc_acct',
+                            references => [ 'svcnum' ]
+                          },
+                        ],
     },
 
     #'svc_wo' => {
@@ -2779,9 +3844,14 @@ sub tables_hashref {
         'totalbytes',  'bigint',     'NULL', '', '', '', 
         'agentnum',    'int',     'NULL', '', '', '', 
       ],
-      'primary_key' => 'prepaynum',
-      'unique'      => [ ['identifier'] ],
-      'index'       => [],
+      'primary_key'  => 'prepaynum',
+      'unique'       => [ ['identifier'] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                        ],
     },
 
     'port' => {
@@ -2791,9 +3861,14 @@ sub tables_hashref {
         'nasport',  'int',     'NULL', '', '', '', 
         'nasnum',   'int',     '',   '', '', '', 
       ],
-      'primary_key' => 'portnum',
-      'unique'      => [],
-      'index'       => [],
+      'primary_key'  => 'portnum',
+      'unique'       => [],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'nasnum' ],
+                            table      => 'nas',
+                          },
+                        ],
     },
 
     'nas' => {
@@ -2809,9 +3884,14 @@ sub tables_hashref {
         'description', 'varchar',     '', 200, 'RADIUS Client', '',
         'svcnum',          'int', 'NULL',  '',              '', '',
       ],
-      'primary_key' => 'nasnum',
-      'unique'      => [ [ 'nasname' ], ],
-      'index'       => [],
+      'primary_key'  => 'nasnum',
+      'unique'       => [ [ 'nasname' ], ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'svc_broadband',
+                          },
+                        ],
     },
 
     'export_nas' => {
@@ -2820,9 +3900,17 @@ sub tables_hashref {
         'exportnum',       'int', '', '', '', '', 
         'nasnum',          'int', '', '', '', '', 
       ],
-      'primary_key' => 'exportnasnum',
-      'unique'      => [ [ 'exportnum', 'nasnum' ] ],
-      'index'       => [ [ 'exportnum' ], [ 'nasnum' ] ],
+      'primary_key'  => 'exportnasnum',
+      'unique'       => [ [ 'exportnum', 'nasnum' ] ],
+      'index'        => [ [ 'exportnum' ], [ 'nasnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'exportnum' ],
+                            table      => 'part_export',
+                          },
+                          { columns    => [ 'nasnum' ],
+                            table      => 'nas',
+                          },
+                        ],
     },
 
     'queue' => {
@@ -2837,11 +3925,19 @@ sub tables_hashref {
         'secure',        'char', 'NULL',       1, '', '',
         'priority',       'int', 'NULL',      '', '', '',
       ],
-      'primary_key' => 'jobnum',
-      'unique'      => [],
-      'index'       => [ [ 'secure' ], [ 'priority' ],
-                         [ 'job' ], [ 'svcnum' ], [ 'custnum' ], [ 'status' ],
-                       ],
+      'primary_key'  => 'jobnum',
+      'unique'       => [],
+      'index'        => [ [ 'secure' ], [ 'priority' ],
+                          [ 'job' ], [ 'svcnum' ], [ 'custnum' ], [ 'status' ],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                        ],
     },
 
     'queue_arg' => {
@@ -2851,9 +3947,14 @@ sub tables_hashref {
         'frozen',      'char', 'NULL',  1, '', '',
         'arg',         'text', 'NULL', '', '', '', 
       ],
-      'primary_key' => 'argnum',
-      'unique'      => [],
-      'index'       => [ [ 'jobnum' ] ],
+      'primary_key'  => 'argnum',
+      'unique'       => [],
+      'index'        => [ [ 'jobnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'jobnum' ],
+                            table      => 'queue',
+                          },
+                        ],
     },
 
     'queue_depend' => {
@@ -2862,9 +3963,18 @@ sub tables_hashref {
         'jobnum',        'bigint', '', '', '', '', 
         'depend_jobnum', 'bigint', '', '', '', '', 
       ],
-      'primary_key' => 'dependnum',
-      'unique'      => [],
-      'index'       => [ [ 'jobnum' ], [ 'depend_jobnum' ] ],
+      'primary_key'  => 'dependnum',
+      'unique'       => [],
+      'index'        => [ [ 'jobnum' ], [ 'depend_jobnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'jobnum' ],
+                            table      => 'queue',
+                          },
+                          { columns    => [ 'depend_jobnum' ],
+                            table      => 'queue',
+                            references => [ 'jobnum' ],
+                          },
+                        ],
     },
 
     'export_svc' => {
@@ -2873,20 +3983,36 @@ sub tables_hashref {
         'exportnum'    => 'int', '', '', '', '', 
         'svcpart'      => 'int', '', '', '', '', 
       ],
-      'primary_key' => 'exportsvcnum',
-      'unique'      => [ [ 'exportnum', 'svcpart' ] ],
-      'index'       => [ [ 'exportnum' ], [ 'svcpart' ] ],
+      'primary_key'  => 'exportsvcnum',
+      'unique'       => [ [ 'exportnum', 'svcpart' ] ],
+      'index'        => [ [ 'exportnum' ], [ 'svcpart' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'exportnum' ],
+                            table      => 'part_export',
+                          },
+                          { columns    => [ 'svcpart' ],
+                            table      => 'part_svc',
+                          },
+                        ],
     },
 
     'export_device' => {
       'columns' => [
         'exportdevicenum' => 'serial', '', '', '', '', 
-        'exportnum'    => 'int', '', '', '', '', 
+        'exportnum'       => 'int', '', '', '', '', 
         'devicepart'      => 'int', '', '', '', '', 
       ],
-      'primary_key' => 'exportdevicenum',
-      'unique'      => [ [ 'exportnum', 'devicepart' ] ],
-      'index'       => [ [ 'exportnum' ], [ 'devicepart' ] ],
+      'primary_key'  => 'exportdevicenum',
+      'unique'       => [ [ 'exportnum', 'devicepart' ] ],
+      'index'        => [ [ 'exportnum' ], [ 'devicepart' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'exportnum' ],
+                            table      => 'part_export',
+                          },
+                          { columns    => [ 'devicepart' ],
+                            table      => 'part_device',
+                          },
+                        ],
     },
 
     'part_export' => {
@@ -2898,9 +4024,15 @@ sub tables_hashref {
         'nodomain',      'char', 'NULL',       1, '', '', 
         'default_machine','int', 'NULL',      '', '', '',
       ],
-      'primary_key' => 'exportnum',
-      'unique'      => [],
-      'index'       => [ [ 'machine' ], [ 'exporttype' ] ],
+      'primary_key'  => 'exportnum',
+      'unique'       => [],
+      'index'        => [ [ 'machine' ], [ 'exporttype' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'default_machine' ],
+                            table      => 'part_export_machine',
+                            references => [ 'machinenum' ]
+                          },
+                        ],
     },
 
     'part_export_option' => {
@@ -2910,23 +4042,36 @@ sub tables_hashref {
         'optionname', 'varchar', '', $char_d, '', '', 
         'optionvalue', 'text', 'NULL', '', '', '', 
       ],
-      'primary_key' => 'optionnum',
-      'unique'      => [],
-      'index'       => [ [ 'exportnum' ], [ 'optionname' ] ],
+      'primary_key'  => 'optionnum',
+      'unique'       => [],
+      'index'        => [ [ 'exportnum' ], [ 'optionname' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'exportnum' ],
+                            table      => 'part_export',
+                          },
+                        ],
     },
 
     'radius_usergroup' => {
       'columns' => [
         'usergroupnum', 'serial', '', '', '', '', 
         'svcnum',       'int', '', '', '', '', 
-        'groupname',    'varchar', 'NULL', $char_d, '', '', 
+        'groupname',    'varchar', 'NULL', $char_d, '', '', #deprecated
         'groupnum',     'int', 'NULL', '', '', '', 
       ],
-      'primary_key' => 'usergroupnum',
-      'unique'      => [],
-      'index'       => [ [ 'svcnum' ], [ 'groupname' ] ],
+      'primary_key'  => 'usergroupnum',
+      'unique'       => [],
+      'index'        => [ [ 'svcnum' ], [ 'groupname' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc', #svc_acct / svc_broadband
+                          },
+                          { columns    => [ 'groupnum' ],
+                            table      => 'radius_group',
+                          },
+                        ],
     },
-    
+
     'radius_group' => {
       'columns' => [
         'groupnum', 'serial', '', '', '', '', 
@@ -2950,9 +4095,14 @@ sub tables_hashref {
         'attrtype',    'char', '',       1, '', '',
         'op',          'char', '',       2, '', '',
       ],
-      'primary_key' => 'attrnum',
-      'unique'      => [],
-      'index'       => [ ['groupnum'], ],
+      'primary_key'  => 'attrnum',
+      'unique'       => [],
+      'index'        => [ ['groupnum'], ],
+      'foreign_keys' => [
+                          { columns    => [ 'groupnum' ],
+                            table      => 'radius_group',
+                          },
+                        ],
     },
 
     'msgcat' => {
@@ -2976,9 +4126,17 @@ sub tables_hashref {
         'month',     'int', '', '', '', '', 
         'amount',   @money_type, '', '', 
       ],
-      'primary_key' => 'exemptnum',
-      'unique'      => [ [ 'custnum', 'taxnum', 'year', 'month' ] ],
-      'index'       => [],
+      'primary_key'  => 'exemptnum',
+      'unique'       => [ [ 'custnum', 'taxnum', 'year', 'month' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'taxnum' ],
+                            table      => 'cust_main_county',
+                          },
+                        ],
     },
 
     'cust_tax_exempt_pkg' => {
@@ -2998,13 +4156,24 @@ sub tables_hashref {
         'exempt_cust_taxname',  'char', 'NULL', 1, '', '',
         'exempt_monthly',       'char', 'NULL', 1, '', '',
       ],
-      'primary_key' => 'exemptpkgnum',
-      'unique' => [],
-      'index'  => [ [ 'taxnum', 'year', 'month' ],
-                    [ 'billpkgnum' ],
-                    [ 'taxnum' ],
-                    [ 'creditbillpkgnum' ],
-                  ],
+      'primary_key'  => 'exemptpkgnum',
+      'unique'       => [],
+      'index'        => [ [ 'taxnum', 'year', 'month' ],
+                          [ 'billpkgnum' ],
+                          [ 'taxnum' ],
+                          [ 'creditbillpkgnum' ],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'billpkgnum' ],
+                            table      => 'cust_bill_pkg',
+                          },
+                          { columns    => [ 'taxnum' ],
+                            table      => 'cust_main_county',
+                          },
+                          { columns    => [ 'creditbillpkgnum' ],
+                            table      => 'cust_credit_bill_pkg',
+                          },
+                        ],
     },
 
     'cust_tax_exempt_pkg_void' => {
@@ -3024,13 +4193,24 @@ sub tables_hashref {
         'exempt_cust_taxname',  'char', 'NULL', 1, '', '',
         'exempt_monthly',       'char', 'NULL', 1, '', '',
       ],
-      'primary_key' => 'exemptpkgnum',
-      'unique' => [],
-      'index'  => [ [ 'taxnum', 'year', 'month' ],
-                    [ 'billpkgnum' ],
-                    [ 'taxnum' ],
-                    [ 'creditbillpkgnum' ],
-                  ],
+      'primary_key'  => 'exemptpkgnum',
+      'unique'       => [],
+      'index'        => [ [ 'taxnum', 'year', 'month' ],
+                          [ 'billpkgnum' ],
+                          [ 'taxnum' ],
+                          [ 'creditbillpkgnum' ],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'billpkgnum' ],
+                            table      => 'cust_bill_pkg_void',
+                          },
+                          { columns    => [ 'taxnum' ],
+                            table      => 'cust_main_county',
+                          },
+                          { columns    => [ 'creditbillpkgnum' ],
+                            table      => 'cust_credit_bill_pkg',
+                          },
+                        ],
     },
 
     'router' => {
@@ -3041,9 +4221,17 @@ sub tables_hashref {
         'agentnum',   'int', 'NULL', '', '', '', 
         'manual_addr', 'char', 'NULL', 1, '', '',
       ],
-      'primary_key' => 'routernum',
-      'unique'      => [],
-      'index'       => [],
+      'primary_key'  => 'routernum',
+      'unique'       => [],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc', #svc_acct / svc_broadband
+                          },
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                        ],
     },
 
     'part_svc_router' => {
@@ -3052,9 +4240,17 @@ sub tables_hashref {
         'svcpart', 'int', '', '', '', '', 
        'routernum', 'int', '', '', '', '', 
       ],
-      'primary_key' => 'svcrouternum',
-      'unique'      => [],
-      'index'       => [],
+      'primary_key'  => 'svcrouternum',
+      'unique'       => [],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'svcpart' ],
+                            table      => 'part_svc',
+                          },
+                          { columns    => [ 'routernum' ],
+                            table      => 'router',
+                          },
+                        ],
     },
 
     'addr_block' => {
@@ -3066,9 +4262,17 @@ sub tables_hashref {
         'agentnum',   'int', 'NULL', '', '', '', 
         'manual_flag', 'char', 'NULL', 1, '', '', 
       ],
-      'primary_key' => 'blocknum',
-      'unique'      => [ [ 'blocknum', 'routernum' ] ],
-      'index'       => [],
+      'primary_key'  => 'blocknum',
+      'unique'       => [ [ 'blocknum', 'routernum' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'routernum' ],
+                            table      => 'router',
+                          },
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                        ],
     },
 
     'svc_broadband' => {
@@ -3096,21 +4300,42 @@ sub tables_hashref {
         'suid',                    'int', 'NULL',        '', '', '',
         'shared_svcnum',           'int', 'NULL',        '', '', '',
       ],
-      'primary_key' => 'svcnum',
-      'unique'      => [ [ 'ip_addr' ], [ 'mac_addr' ] ],
-      'index'       => [],
+      'primary_key'  => 'svcnum',
+      'unique'       => [ [ 'ip_addr' ], [ 'mac_addr' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                          { columns    => [ 'routernum' ],
+                            table      => 'router',
+                          },
+                          { columns    => [ 'blocknum' ],
+                            table      => 'addr_block',
+                          },
+                          { columns    => [ 'sectornum' ],
+                            table      => 'tower_sector',
+                          },
+                          { columns    => [ 'shared_svcnum' ],
+                            table      => 'svc_broadband',
+                            references => [ 'svcnum' ],
+                          },
+                        ],
     },
 
     'tower' => {
       'columns' => [
-        'towernum',   'serial',     '',      '', '', '',
-        #'agentnum',      'int', 'NULL',      '', '', '',
-        'towername', 'varchar',     '', $char_d, '', '',
-        'disabled',     'char', 'NULL',       1, '', '',
-        'latitude', 'decimal', 'NULL',   '10,7', '', '', 
-        'longitude','decimal', 'NULL',   '10,7', '', '', 
-        'altitude', 'decimal', 'NULL',       '', '', '', 
-        'coord_auto',  'char', 'NULL',        1, '', '',
+        'towernum',    'serial',     '',      '', '', '',
+        #'agentnum',       'int', 'NULL',      '', '', '',
+        'towername',  'varchar',     '', $char_d, '', '',
+        'disabled',      'char', 'NULL',       1, '', '',
+        'latitude',   'decimal', 'NULL',  '10,7', '', '', 
+        'longitude',  'decimal', 'NULL',  '10,7', '', '', 
+        'coord_auto',    'char', 'NULL',       1, '', '',
+        'altitude',   'decimal', 'NULL',      '', '', '', 
+        'height',     'decimal', 'NULL',      '', '', '', 
+        'veg_height', 'decimal', 'NULL',      '', '', '', 
+        'color',      'varchar', 'NULL',       6, '', '',
       ],
       'primary_key' => 'towernum',
       'unique'      => [ [ 'towername' ] ], # , 'agentnum' ] ],
@@ -3123,10 +4348,21 @@ sub tables_hashref {
         'towernum',       'int',     '',      '', '', '',
         'sectorname', 'varchar',     '', $char_d, '', '',
         'ip_addr',    'varchar', 'NULL',      15, '', '',
-      ],
-      'primary_key' => 'sectornum',
-      'unique'      => [ [ 'towernum', 'sectorname' ], [ 'ip_addr' ], ],
-      'index'       => [ [ 'towernum' ] ],
+        'height',     'decimal', 'NULL',      '', '', '', 
+        'freq_mhz',       'int', 'NULL',      '', '', '',
+        'direction',      'int', 'NULL',      '', '', '',
+        'width',          'int', 'NULL',      '', '', '',
+        #downtilt etc? rfpath has profile files for devices/antennas you upload?
+        'range',      'decimal', 'NULL',      '', '', '',  #?
+      ],
+      'primary_key'  => 'sectornum',
+      'unique'       => [ [ 'towernum', 'sectorname' ], [ 'ip_addr' ], ],
+      'index'        => [ [ 'towernum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'towernum' ],
+                            table      => 'tower',
+                          },
+                        ],
     },
 
     'part_virtual_field' => {
@@ -3149,9 +4385,14 @@ sub tables_hashref {
         'vfieldpart', 'int', '', '', '', '', 
         'value', 'varchar', '', 128, '', '', 
       ],
-      'primary_key' => 'vfieldnum',
-      'unique' => [ [ 'vfieldpart', 'recnum' ] ],
-      'index' => [],
+      'primary_key'  => 'vfieldnum',
+      'unique'       => [ [ 'vfieldpart', 'recnum' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'vfieldpart' ],
+                            table      => 'part_virtual_field',
+                          },
+                        ],
     },
 
     'acct_snarf' => {
@@ -3169,9 +4410,14 @@ sub tables_hashref {
         'tls',           'char', 'NULL',       1, '', '', 
         'mailbox',    'varchar', 'NULL', $char_d, '', '', 
       ],
-      'primary_key' => 'snarfnum',
-      'unique' => [],
-      'index'  => [ [ 'svcnum' ] ],
+      'primary_key'  => 'snarfnum',
+      'unique'       => [],
+      'index'        => [ [ 'svcnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'svc_acct',
+                          },
+                        ],
     },
 
     'svc_external' => {
@@ -3180,9 +4426,14 @@ sub tables_hashref {
         'id',      'bigint', 'NULL',      '', '', '', 
         'title',  'varchar', 'NULL', $char_d, '', '', 
       ],
-      'primary_key' => 'svcnum',
-      'unique'      => [],
-      'index'       => [],
+      'primary_key'  => 'svcnum',
+      'unique'       => [],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                        ],
     },
 
     'cust_pay_refund' => {
@@ -3193,9 +4444,17 @@ sub tables_hashref {
         '_date',    @date_type, '', '', 
         'amount',   @money_type, '', '', 
       ],
-      'primary_key' => 'payrefundnum',
-      'unique' => [],
-      'index' => [ ['paynum'], ['refundnum'] ],
+      'primary_key'  => 'payrefundnum',
+      'unique'       => [],
+      'index'        => [ ['paynum'], ['refundnum'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'paynum' ],
+                            table      => 'cust_pay',
+                          },
+                          { columns    => [ 'refundnum' ],
+                            table      => 'cust_refund',
+                          },
+                        ],
     },
 
     'part_pkg_option' => {
@@ -3205,9 +4464,14 @@ sub tables_hashref {
         'optionname', 'varchar', '', $char_d, '', '', 
         'optionvalue', 'text', 'NULL', '', '', '', 
       ],
-      'primary_key' => 'optionnum',
-      'unique'      => [],
-      'index'       => [ [ 'pkgpart' ], [ 'optionname' ] ],
+      'primary_key'  => 'optionnum',
+      'unique'       => [],
+      'index'        => [ [ 'pkgpart' ], [ 'optionname' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgpart' ],
+                            table      => 'part_pkg',
+                          },
+                        ],
     },
 
     'part_pkg_vendor' => {
@@ -3217,11 +4481,19 @@ sub tables_hashref {
         'exportnum', 'int', '', '', '', '', 
         'vendor_pkg_id', 'varchar', '', $char_d, '', '', 
       ],
-      'primary_key' => 'num',
-      'unique' => [ [ 'pkgpart', 'exportnum' ] ],
-      'index'       => [ [ 'pkgpart' ] ],
+      'primary_key'  => 'num',
+      'unique'       => [ [ 'pkgpart', 'exportnum' ] ],
+      'index'        => [ [ 'pkgpart' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgpart' ],
+                            table      => 'part_pkg',
+                          },
+                          { columns    => [ 'exportnum' ],
+                            table      => 'part_export',
+                          },
+                        ],
     },
-    
+
     'part_pkg_report_option' => {
       'columns' => [
         'num',      'serial',   '',      '', '', '', 
@@ -3243,9 +4515,14 @@ sub tables_hashref {
         'rollover', 'char', 'NULL',  1, '', '',
         'description',  'varchar', 'NULL', $char_d, '', '',
       ],
-      'primary_key' => 'pkgusagepart',
-      'unique'      => [],
-      'index'       => [ [ 'pkgpart' ] ],
+      'primary_key'  => 'pkgusagepart',
+      'unique'       => [],
+      'index'        => [ [ 'pkgpart' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgpart' ],
+                            table      => 'part_pkg',
+                          },
+                        ],
     },
 
     'part_pkg_usage_class' => {
@@ -3254,9 +4531,17 @@ sub tables_hashref {
         'pkgusagepart', 'int',  '', '', '', '',
         'classnum',     'int','NULL', '', '', '',
       ],
-      'primary_key' => 'num',
-      'unique'      => [ [ 'pkgusagepart', 'classnum' ] ],
-      'index'       => [],
+      'primary_key'  => 'num',
+      'unique'       => [ [ 'pkgusagepart', 'classnum' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgusagepart' ],
+                            table      => 'part_pkg_usage',
+                          },
+                          { columns    => [ 'classnum' ],
+                            table      => 'usage_class',
+                          },
+                        ],
     },
 
     'rate' => {
@@ -3285,11 +4570,33 @@ sub tables_hashref {
         'cdrtypenum',      'int', 'NULL',     '',       '', '',
         'region_group', 'char', 'NULL',        1,       '', '', 
       ],
-      'primary_key' => 'ratedetailnum',
-      'unique'      => [ [ 'ratenum', 'orig_regionnum', 'dest_regionnum' ] ],
-      'index'       => [ [ 'ratenum', 'dest_regionnum' ],
-                         [ 'ratenum', 'ratetimenum' ]
-                       ],
+      'primary_key'  => 'ratedetailnum',
+      'unique'       => [ [ 'ratenum', 'orig_regionnum', 'dest_regionnum' ] ],
+      'index'        => [ [ 'ratenum', 'dest_regionnum' ],
+                          [ 'ratenum', 'ratetimenum' ]
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'ratenum' ],
+                            table      => 'rate',
+                          },
+                          { columns    => [ 'orig_regionnum' ],
+                            table      => 'rate_region',
+                            references => [ 'regionnum' ],
+                          },
+                          { columns    => [ 'dest_regionnum' ],
+                            table      => 'rate_region',
+                            references => [ 'regionnum' ],
+                          },
+                          { columns    => [ 'ratetimenum' ],
+                            table      => 'rate_time',
+                          },
+                          { columns    => [ 'classnum' ],
+                            table      => 'usage_class',
+                          },
+                          { columns    => [ 'cdrtypenum' ],
+                            table      => 'cdr_type',
+                          },
+                        ],
     },
 
     'rate_region' => {
@@ -3314,9 +4621,17 @@ sub tables_hashref {
         'state',       'char',    'NULL',       2, '', '', 
         'ocn',         'char',    'NULL',       4, '', '', 
       ],
-      'primary_key' => 'prefixnum',
-      'unique'      => [],
-      'index'       => [ [ 'countrycode' ], [ 'npa' ], [ 'regionnum' ] ],
+      'primary_key'  => 'prefixnum',
+      'unique'       => [],
+      'index'        => [ [ 'countrycode' ], [ 'npa' ], [ 'regionnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'regionnum' ],
+                            table      => 'rate_region',
+                          },
+                          { columns    => [ 'latanum' ],
+                            table      => 'lata',
+                          },
+                        ],
     },
 
     'rate_time' => {
@@ -3336,10 +4651,15 @@ sub tables_hashref {
         'etime',          'int', '', '', '', '',
         'ratetimenum',    'int', '', '', '', '',
       ],
-      'primary_key' => 'intervalnum',
-      'unique'      => [],
-      'index'       => [],
-    },
+      'primary_key'  => 'intervalnum',
+      'unique'       => [],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'ratetimenum' ],
+                            table      => 'rate_time',
+                          },
+                        ],
+     },
 
     #not really part of the above rate_ stuff (used with flat rate rather than
     # rated billing), but could be eventually, and its a rate
@@ -3360,9 +4680,14 @@ sub tables_hashref {
         'min_quan',         'int', '',     '', '', '',
         'min_charge',   'decimal', '', '10,4', '', '',
       ],
-      'primary_key' => 'tierdetailnum',
-      'unique'      => [],
-      'index'       => [ ['tiernum'], ],
+      'primary_key'  => 'tierdetailnum',
+      'unique'       => [],
+      'index'        => [ ['tiernum'], ],
+      'foreign_keys' => [
+                          { columns    => [ 'tiernum' ],
+                            table      => 'rate_tier',
+                          },
+                        ],
     },
 
     'usage_class' => {
@@ -3384,10 +4709,15 @@ sub tables_hashref {
         'code',      'varchar',   '', $char_d, '', '', 
         'agentnum',  'int',       '', '', '', '', 
       ],
-      'primary_key' => 'codenum',
-      'unique'      => [ [ 'agentnum', 'code' ] ],
-      'index'       => [ [ 'agentnum' ] ],
-    },
+      'primary_key'  => 'codenum',
+      'unique'       => [ [ 'agentnum', 'code' ] ],
+      'index'        => [ [ 'agentnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                        ],
+     },
 
     'reg_code_pkg' => {
       'columns' => [
@@ -3395,9 +4725,17 @@ sub tables_hashref {
         'codenum',   'int',    '', '', '', '', 
         'pkgpart',   'int',    '', '', '', '', 
       ],
-      'primary_key' => 'codepkgnum',
-      'unique'      => [ [ 'codenum', 'pkgpart' ] ],
-      'index'       => [ [ 'codenum' ] ],
+      'primary_key'  => 'codepkgnum',
+      'unique'       => [ [ 'codenum', 'pkgpart' ] ],
+      'index'        => [ [ 'codenum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'codenum' ],
+                            table      => 'reg_code',
+                          },
+                          { columns    => [ 'pkgpart' ],
+                            table      => 'part_pkg',
+                          },
+                        ],
     },
 
     'clientapi_session' => {
@@ -3418,9 +4756,14 @@ sub tables_hashref {
         'fieldname',  'varchar',     '', $char_d, '', '', 
         'fieldvalue',    'text', 'NULL', '', '', '', 
       ],
-      'primary_key' => 'fieldnum',
-      'unique'      => [ [ 'sessionnum', 'fieldname' ] ],
-      'index'       => [],
+      'primary_key'  => 'fieldnum',
+      'unique'       => [ [ 'sessionnum', 'fieldname' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'sessionnum' ],
+                            table      => 'clientapi_session',
+                          },
+                        ],
     },
 
     'payment_gateway' => {
@@ -3447,9 +4790,14 @@ sub tables_hashref {
         'optionname',  'varchar', '',     $char_d, '', '', 
         'optionvalue', 'text',    'NULL', '', '', '', 
       ],
-      'primary_key' => 'optionnum',
-      'unique'      => [],
-      'index'       => [ [ 'gatewaynum' ], [ 'optionname' ] ],
+      'primary_key'  => 'optionnum',
+      'unique'       => [],
+      'index'        => [ [ 'gatewaynum' ], [ 'optionname' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'gatewaynum' ],
+                            table      => 'payment_gateway',
+                          },
+                        ],
     },
 
     'agent_payment_gateway' => {
@@ -3460,9 +4808,19 @@ sub tables_hashref {
         'cardtype',        'varchar', 'NULL', $char_d, '', '', 
         'taxclass',        'varchar', 'NULL', $char_d, '', '', 
       ],
-      'primary_key' => 'agentgatewaynum',
-      'unique'      => [],
-      'index'       => [ [ 'agentnum', 'cardtype' ], ],
+      'primary_key'  => 'agentgatewaynum',
+      'unique'       => [],
+      'index'        => [ [ 'agentnum', 'cardtype' ], ],
+
+      'foreign_keys' => [
+
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                          { columns    => [ 'gatewaynum' ],
+                            table      => 'payment_gateway',
+                          },
+                        ],
     },
 
     'banned_pay' => {
@@ -3478,9 +4836,14 @@ sub tables_hashref {
         'bantype', 'varchar',  'NULL', $char_d, '', '',
         'reason',  'varchar',  'NULL', $char_d, '', '', 
       ],
-      'primary_key' => 'bannum',
-      'unique'      => [],
-      'index'       => [ [ 'payby', 'payinfo' ], [ 'usernum' ], ],
+      'primary_key'  => 'bannum',
+      'unique'       => [],
+      'index'        => [ [ 'payby', 'payinfo' ], [ 'usernum' ], ],
+      'foreign_keys' => [
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                        ],
     },
 
     'pkg_category' => {
@@ -3504,9 +4867,14 @@ sub tables_hashref {
         'disabled',    'char', 'NULL',       1, '', '', 
         'fcc_ds0s',      'int',     'NULL', '', '', '', 
       ],
-      'primary_key' => 'classnum',
-      'unique' => [],
-      'index' => [ ['disabled'] ],
+      'primary_key'  => 'classnum',
+      'unique'       => [],
+      'index'        => [ ['disabled'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'categorynum' ],
+                            table      => 'pkg_category',
+                          },
+                        ],
     },
 
     'cdr' => {
@@ -3588,7 +4956,7 @@ sub tables_hashref {
         ###
         'servicecode',             'int', 'NULL',      '', '', '',
         'quantity_able',           'int', 'NULL',      '', '', '', 
-        
+
         ###
         #and now for our own fields
         ###
@@ -3648,6 +5016,8 @@ sub tables_hashref {
                    [ 'cdrbatch' ], [ 'cdrbatchnum' ],
                    [ 'src_ip_addr' ], [ 'dst_ip_addr' ], [ 'dst_term' ],
                  ],
+      #no FKs on cdr table... choosing not to throw errors no matter what's
+      # thrown in here.  better to have the data.
     },
 
     'cdr_batch' => {
@@ -3672,9 +5042,17 @@ sub tables_hashref {
         'status',       'varchar', 'NULL',      32, '', '',
         'svcnum',           'int', 'NULL',      '', '', '',
       ],
-      'primary_key' => 'cdrtermnum',
-      'unique'      => [ [ 'acctid', 'termpart' ] ],
-      'index'       => [ [ 'acctid' ], [ 'status' ], ],
+      'primary_key'  => 'cdrtermnum',
+      'unique'       => [ [ 'acctid', 'termpart' ] ],
+      'index'        => [ [ 'acctid' ], [ 'status' ], ],
+      'foreign_keys' => [
+                          { columns    => [ 'acctid' ],
+                            table      => 'cdr',
+                          },
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                        ],
     },
 
     #to handle multiple termination/settlement passes...
@@ -3745,9 +5123,20 @@ sub tables_hashref {
         'svcnum',    'int',     'NULL',      '', '', '',
         'svc_field', 'varchar', 'NULL', $char_d, '', '',
       ],
-      'primary_key' => 'itemnum',
-      'unique' => [ [ 'classnum', 'item' ] ],
-      'index'  => [ [ 'classnum' ], [ 'agentnum' ], [ 'svcnum' ] ],
+      'primary_key'  => 'itemnum',
+      'unique'       => [ [ 'classnum', 'item' ] ],
+      'index'        => [ [ 'classnum' ], [ 'agentnum' ], [ 'svcnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'classnum' ],
+                            table      => 'inventory_class',
+                          },
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                        ],
     },
 
     'inventory_class' => {
@@ -3768,9 +5157,14 @@ sub tables_hashref {
         'start_date', @date_type,               '', '',
         'last_date',  @date_type,               '', '',
       ],
-      'primary_key' => 'sessionnum',
-      'unique' => [ [ 'sessionkey' ] ],
-      'index'  => [],
+      'primary_key'  => 'sessionnum',
+      'unique'       => [ [ 'sessionkey' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                        ],
     },
 
     'access_user' => {
@@ -3782,11 +5176,22 @@ sub tables_hashref {
         'last',               'varchar', 'NULL', $char_d, '', '', 
         'first',              'varchar', 'NULL', $char_d, '', '', 
         'user_custnum',           'int', 'NULL',      '', '', '',
+        'report_salesnum',        'int', 'NULL',      '', '', '',
         'disabled',              'char', 'NULL',       1, '', '', 
       ],
-      'primary_key' => 'usernum',
-      'unique' => [ [ 'username' ] ],
-      'index'  => [ [ 'user_custnum' ] ],
+      'primary_key'  => 'usernum',
+      'unique'       => [ [ 'username' ] ],
+      'index'        => [ [ 'user_custnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'user_custnum' ],
+                            table      => 'cust_main',
+                            references => [ 'custnum' ],
+                          },
+                          { columns    => [ 'report_salesnum' ],
+                            table      => 'sales',
+                            references => [ 'salesnum' ],
+                          },
+                        ],
     },
 
     'access_user_pref' => {
@@ -3797,9 +5202,14 @@ sub tables_hashref {
         'prefvalue', 'text', 'NULL', '', '', '', 
         'expiration', @date_type, '', '',
       ],
-      'primary_key' => 'prefnum',
-      'unique' => [],
-      'index'  => [ [ 'usernum' ] ],
+      'primary_key'  => 'prefnum',
+      'unique'       => [],
+      'index'        => [ [ 'usernum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                        ],
     },
 
     'access_group' => {
@@ -3818,10 +5228,18 @@ sub tables_hashref {
         'usernum',         'int', '', '', '', '',
         'groupnum',        'int', '', '', '', '',
       ],
-      'primary_key' => 'usergroupnum',
-      'unique' => [ [ 'usernum', 'groupnum' ] ],
-      'index'  => [ [ 'usernum' ] ],
-    },
+      'primary_key'  => 'usergroupnum',
+      'unique'       => [ [ 'usernum', 'groupnum' ] ],
+      'index'        => [ [ 'usernum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'usernum' ],
+                            table      => 'access_user',
+                          },
+                          { columns    => [ 'groupnum' ],
+                            table      => 'access_group',
+                          },
+                        ],
+     },
 
     'access_groupagent' => {
       'columns' => [
@@ -3829,9 +5247,17 @@ sub tables_hashref {
         'groupnum',         'int', '', '', '', '',
         'agentnum',         'int', '', '', '', '',
       ],
-      'primary_key' => 'groupagentnum',
-      'unique' => [ [ 'groupnum', 'agentnum' ] ],
-      'index'  => [ [ 'groupnum' ] ],
+      'primary_key'  => 'groupagentnum',
+      'unique'       => [ [ 'groupnum', 'agentnum' ] ],
+      'index'        => [ [ 'groupnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'groupnum' ],
+                            table      => 'access_group',
+                          },
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                        ],
     },
 
     'access_right' => {
@@ -3872,11 +5298,31 @@ sub tables_hashref {
         'sms_account',                'varchar', 'NULL', $char_d, '', '',
         'max_simultaneous',               'int', 'NULL',      '', '', '',
       ],
-      'primary_key' => 'svcnum',
-      'unique' => [ [ 'sms_carrierid', 'sms_account'] ],
-      'index'  => [ ['countrycode', 'phonenum'], ['pbxsvc'], ['domsvc'],
-                    ['locationnum'], ['sms_carrierid'],
-                  ],
+      'primary_key'  => 'svcnum',
+      'unique'       => [ [ 'sms_carrierid', 'sms_account'] ],
+      'index'        => [ ['countrycode', 'phonenum'], ['pbxsvc'], ['domsvc'],
+                          ['locationnum'], ['sms_carrierid'],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                          { columns    => [ 'pbxsvc' ],
+                            table      => 'svc_pbx', #'cust_svc',
+                            references => [ 'svcnum' ],
+                          },
+                          { columns    => [ 'domsvc' ],
+                            table      => 'svc_domain', #'cust_svc',
+                            references => [ 'svcnum' ],
+                          },
+                          { columns    => [ 'locationnum' ],
+                            table      => 'cust_location',
+                          },
+                          { columns    => [ 'sms_carrierid' ],
+                            table      => 'cdr_carrier',
+                            references => [ 'carrierid' ],
+                          },
+                        ],
     },
 
     'phone_device' => {
@@ -3886,9 +5332,17 @@ sub tables_hashref {
         'svcnum',       'int',     '', '', '', '', 
         'mac_addr', 'varchar', 'NULL', 12, '', '', 
       ],
-      'primary_key' => 'devicenum',
-      'unique' => [ [ 'mac_addr' ], ],
-      'index'  => [ [ 'devicepart' ], [ 'svcnum' ], ],
+      'primary_key'  => 'devicenum',
+      'unique'       => [ [ 'mac_addr' ], ],
+      'index'        => [ [ 'devicepart' ], [ 'svcnum' ], ],
+      'foreign_keys' => [
+                          { columns    => [ 'devicepart' ],
+                            table      => 'part_device',
+                          },
+                          { columns    => [ 'svcnum' ],
+                            table      => 'svc_phone',
+                          },
+                        ],
     },
 
     'part_device' => {
@@ -3897,9 +5351,15 @@ sub tables_hashref {
         'devicename', 'varchar', '', $char_d, '', '',
         'inventory_classnum', 'int', 'NULL', '', '', '',
       ],
-      'primary_key' => 'devicepart',
-      'unique' => [ [ 'devicename' ] ], #?
-      'index'  => [],
+      'primary_key'  => 'devicepart',
+      'unique'       => [ [ 'devicename' ] ], #?
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'inventory_classnum' ],
+                            table      => 'inventory_class',
+                            references => [ 'classnum' ],
+                          },
+                        ],
     },
 
     'phone_avail' => {
@@ -3919,18 +5379,35 @@ sub tables_hashref {
         'svcnum',      'int',     'NULL',      '', '', '',
         'availbatch', 'varchar',  'NULL', $char_d, '', '',
       ],
-      'primary_key' => 'availnum',
-      'unique' => [],
-      'index'  => [ [ 'exportnum', 'countrycode', 'state' ],     #npa search
-                    [ 'exportnum', 'countrycode', 'npa' ],       #nxx search
-                    [ 'exportnum', 'countrycode', 'npa', 'nxx' ],#station search
-                    [ 'exportnum', 'countrycode', 'npa', 'nxx', 'station' ], # #
-                    [ 'svcnum' ],
-                    [ 'availbatch' ],
-                    [ 'latanum' ],
-                  ],
+      'primary_key'  => 'availnum',
+      'unique'       => [],
+      'index'        => [ ['exportnum','countrycode','state'],    #npa search
+                          ['exportnum','countrycode','npa'],      #nxx search
+                          ['exportnum','countrycode','npa','nxx'],#station srch
+                          [ 'exportnum','countrycode','npa','nxx','station'], #
+                          [ 'svcnum' ],
+                          [ 'availbatch' ],
+                          [ 'latanum' ],
+                        ],
+      'foreign_keys' => [
+                          { columns    => [ 'exportnum' ],
+                            table      => 'part_export',
+                          },
+                          { columns    => [ 'latanum' ],
+                            table      => 'lata',
+                          },
+                          { columns    => [ 'msanum' ],
+                            table      => 'msa',
+                          },
+                          { columns    => [ 'ordernum' ],
+                            table      => 'did_order',
+                          },
+                          { columns    => [ 'svcnum' ],
+                            table      => 'svc_phone',
+                          },
+                        ],
     },
-    
+
     'lata' => {
       'columns' => [
         'latanum',    'int',      '',      '', '', '', 
@@ -3941,7 +5418,7 @@ sub tables_hashref {
       'unique' => [],
       'index'  => [],
     },
-    
+
     'msa' => {
       'columns' => [
         'msanum',    'int',      '',      '', '', '', 
@@ -3951,7 +5428,7 @@ sub tables_hashref {
       'unique' => [],
       'index'  => [],
     },
-    
+
     'rate_center' => {
       'columns' => [
         'ratecenternum',    'serial',      '',      '', '', '', 
@@ -3971,7 +5448,7 @@ sub tables_hashref {
       'unique' => [],
       'index'  => [],
     },
-    
+
     'did_order_item' => {
       'columns' => [
         'orderitemnum',    'serial',      '',      '', '', '', 
@@ -3984,9 +5461,26 @@ sub tables_hashref {
         'quantity',      'int',     '',      '', '', '',
         'custnum',   'int', 'NULL', '', '', '',
       ],
-      'primary_key' => 'orderitemnum',
-      'unique' => [],
-      'index'  => [],
+      'primary_key'  => 'orderitemnum',
+      'unique'       => [],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'ordernum' ],
+                            table      => 'did_order',
+                          },
+                          { columns    => [ 'msanum' ],
+                            table      => 'msa',
+                          },
+                          { columns    => [ 'latanum' ],
+                            table      => 'lata',
+                          },
+                          { columns    => [ 'ratecenternum' ],
+                            table      => 'rate_center',
+                          },
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                        ],
     },
 
     'did_order' => {
@@ -3999,9 +5493,17 @@ sub tables_hashref {
         'confirmed',      'int',     'NULL',      '', '', '',
         'received',      'int',     'NULL',      '', '', '',
       ],
-      'primary_key' => 'ordernum',
-      'unique' => [ [ 'vendornum', 'vendor_order_id' ] ],
-      'index'  => [],
+      'primary_key'  => 'ordernum',
+      'unique'       => [ [ 'vendornum', 'vendor_order_id' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'vendornum' ],
+                            table      => 'did_vendor',
+                          },
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                        ],
     },
 
     'reason_type' => {
@@ -4024,9 +5526,19 @@ sub tables_hashref {
         'unsuspend_pkgpart', 'int',  'NULL', '', '', '',
         'unsuspend_hold','char',    'NULL', 1, '', '',
       ],
-      'primary_key' => 'reasonnum',
-      'unique' => [],
-      'index' => [],
+      'primary_key'  => 'reasonnum',
+      'unique'       => [],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'reason_type' ],
+                            table      => 'reason_type',
+                            references => [ 'typenum' ],
+                          },
+                          { columns    => [ 'unsuspend_pkgpart' ],
+                            table      => 'part_pkg',
+                            references => [ 'pkgpart' ],
+                          },
+                        ],
     },
 
     'conf' => {
@@ -4037,9 +5549,14 @@ sub tables_hashref {
         'name',     'varchar',    '', $char_d, '', '', 
         'value',    'text',   'NULL',      '', '', '',
       ],
-      'primary_key' => 'confnum',
-      'unique' => [ [ 'agentnum', 'locale', 'name' ] ],
-      'index' => [],
+      'primary_key'  => 'confnum',
+      'unique'       => [ [ 'agentnum', 'locale', 'name' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                        ],
     },
 
     'pkg_referral' => {
@@ -4048,9 +5565,17 @@ sub tables_hashref {
         'pkgnum',        'int',    '', '', '', '',
         'refnum',        'int',    '', '', '', '',
       ],
-      'primary_key' => 'pkgrefnum',
-      'unique'      => [ [ 'pkgnum', 'refnum' ] ],
-      'index'       => [ [ 'pkgnum' ], [ 'refnum' ] ],
+      'primary_key'  => 'pkgrefnum',
+      'unique'       => [ [ 'pkgnum', 'refnum' ] ],
+      'index'        => [ [ 'pkgnum' ], [ 'refnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                          { columns    => [ 'refnum' ],
+                            table      => 'part_referral',
+                          },
+                        ],
     },
 
     'svc_pbx' => {
@@ -4061,9 +5586,14 @@ sub tables_hashref {
         'max_extensions',   'int', 'NULL',      '', '', '',
         'max_simultaneous', 'int', 'NULL',      '', '', '',
       ],
-      'primary_key' => 'svcnum',
-      'unique' => [],
-      'index'  => [ [ 'id' ] ],
+      'primary_key'  => 'svcnum',
+      'unique'       => [],
+      'index'        => [ [ 'id' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                        ],
     },
 
     'svc_mailinglist' => { #svc_group?
@@ -4077,9 +5607,21 @@ sub tables_hashref {
         'reject_auto',      'char', 'NULL',             1, '', '',#RejectAuto
         'remove_to_and_cc', 'char', 'NULL',             1, '', '',#RemoveToAndCc
       ],
-      'primary_key' => 'svcnum',
-      'unique' => [],
-      'index'  => [ ['username'], ['domsvc'], ['listnum'] ],
+      'primary_key'  => 'svcnum',
+      'unique'       => [],
+      'index'        => [ ['username'], ['domsvc'], ['listnum'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                          { columns    => [ 'domsvc' ],
+                            table      => 'svc_domain', #'cust_svc',
+                            references => [ 'svcnum' ],
+                          },
+                          { columns    => [ 'listnum' ],
+                            table      => 'mailinglist',
+                          },
+                        ],
     },
 
     'mailinglist' => {
@@ -4100,9 +5642,20 @@ sub tables_hashref {
         'contactemailnum',     'int', 'NULL',   '', '', '', 
         'email',           'varchar', 'NULL',  255, '', '', 
       ],
-      'primary_key' => 'membernum',
-      'unique'      => [],
-      'index'       => [['listnum'],['svcnum'],['contactemailnum'],['email']],
+      'primary_key'  => 'membernum',
+      'unique'       => [],
+      'index'        => [['listnum'],['svcnum'],['contactemailnum'],['email']],
+      'foreign_keys' => [
+                          { columns    => [ 'listnum' ],
+                            table      => 'mailinglist',
+                          },
+                          { columns    => [ 'svcnum' ],
+                            table      => 'svc_acct',
+                          },
+                          { columns    => [ 'contactemailnum' ],
+                            table      => 'contact_email',
+                          },
+                        ],
     },
 
     'bill_batch' => {
@@ -4112,9 +5665,14 @@ sub tables_hashref {
         'status',             'char', 'NULL', '1', '', '',
         'pdf',                'blob', 'NULL',  '', '', '',
       ],
-      'primary_key' => 'batchnum',
-      'unique'      => [],
-      'index'       => [ ['agentnum'] ],
+      'primary_key'  => 'batchnum',
+      'unique'       => [],
+      'index'        => [ ['agentnum'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                        ],
     },
 
     'cust_bill_batch' => {
@@ -4123,9 +5681,17 @@ sub tables_hashref {
         'batchnum',            'int',     '', '', '', '',
         'invnum',              'int',     '', '', '', '',
       ],
-      'primary_key' => 'billbatchnum',
-      'unique'      => [],
-      'index'       => [ [ 'batchnum' ], [ 'invnum' ] ],
+      'primary_key'  => 'billbatchnum',
+      'unique'       => [],
+      'index'        => [ [ 'batchnum' ], [ 'invnum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'batchnum' ],
+                            table      => 'bill_batch',
+                          },
+                          { columns    => [ 'invnum' ],
+                            table      => 'cust_bill',
+                          },
+                        ],
     },
 
     'cust_bill_batch_option' => {
@@ -4135,10 +5701,15 @@ sub tables_hashref {
         'optionname', 'varchar', '', $char_d, '', '', 
         'optionvalue', 'text', 'NULL', '', '', '', 
       ],
-      'primary_key' => 'optionnum',
-      'unique'      => [],
-      'index'       => [ [ 'billbatchnum' ], [ 'optionname' ] ],
-    },
+      'primary_key'  => 'optionnum',
+      'unique'       => [],
+      'index'        => [ [ 'billbatchnum' ], [ 'optionname' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'billbatchnum' ],
+                            table      => 'cust_bill_batch',
+                          },
+                        ],
+     },
 
     'msg_template' => {
       'columns' => [
@@ -4152,9 +5723,14 @@ sub tables_hashref {
         'from_addr', 'varchar', 'NULL',     255, '', '',
         'bcc_addr',  'varchar', 'NULL',     255, '', '',
       ],
-      'primary_key' => 'msgnum',
-      'unique'      => [ ],
-      'index'       => [ ['agentnum'], ],
+      'primary_key'  => 'msgnum',
+      'unique'       => [ ],
+      'index'        => [ ['agentnum'], ],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                        ],
     },
 
     'template_content' => {
@@ -4165,9 +5741,14 @@ sub tables_hashref {
         'subject',   'varchar', 'NULL',     512, '', '',
         'body',         'text', 'NULL',      '', '', '',
       ],
-      'primary_key' => 'contentnum',
-      'unique'      => [ ['msgnum', 'locale'] ],
-      'index'       => [ ],
+      'primary_key'  => 'contentnum',
+      'unique'       => [ ['msgnum', 'locale'] ],
+      'index'        => [ ],
+      'foreign_keys' => [
+                          { columns    => [ 'msgnum' ],
+                            table      => 'msg_template',
+                          },
+                        ],
     },
 
     'cust_msg' => {
@@ -4183,9 +5764,17 @@ sub tables_hashref {
         'error',     'varchar', 'NULL',    255, '', '',
         'status',    'varchar',     '',$char_d, '', '',
       ],
-      'primary_key' => 'custmsgnum',
-      'unique'      => [ ],
-      'index'       => [ ['custnum'], ],
+      'primary_key'  => 'custmsgnum',
+      'unique'       => [ ],
+      'index'        => [ ['custnum'], ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'msgnum' ],
+                            table      => 'msg_template',
+                          },
+                        ],
     },
 
     'svc_cert' => {
@@ -4204,9 +5793,17 @@ sub tables_hashref {
         'country',              'char', 'NULL',       2, '', '',
         'cert_contact',      'varchar', 'NULL', $char_d, '', '',
       ],
-      'primary_key' => 'svcnum',
-      'unique' => [],
-      'index'  => [], #recnum
+      'primary_key'  => 'svcnum',
+      'unique'       => [],
+      'index'        => [], #recnum
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                          { columns    => [ 'recnum' ],
+                            table      => 'domain_record',
+                          },
+                        ],
     },
 
     'svc_port' => {
@@ -4214,9 +5811,14 @@ sub tables_hashref {
         'svcnum',                'int',     '',      '', '', '', 
         'serviceid', 'varchar', '', 64, '', '', #srvexport / reportfields
       ],
-      'primary_key' => 'svcnum',
-      'unique' => [],
-      'index'  => [], #recnum
+      'primary_key'  => 'svcnum',
+      'unique'       => [],
+      'index'        => [], #recnum
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                        ],
     },
 
     'areacode'  => {
@@ -4258,9 +5860,14 @@ sub tables_hashref {
         'subject', 'varchar', 'NULL', '255', '', '',
         'handling', 'varchar', 'NULL', $char_d, '', '',
       ],
-      'primary_key' => 'targetnum',
-      'unique' => [ [ 'targetnum' ] ],
-      'index' => [],
+      'primary_key'   => 'targetnum',
+      'unique'        => [ [ 'targetnum' ] ],
+      'index'         => [],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                        ],
     },
 
     'log' => {
@@ -4273,9 +5880,14 @@ sub tables_hashref {
         'level',      'int',  '', '', '', '',
         'message',    'text', '', '', '', '',
       ],
-      'primary_key' => 'lognum',
-      'unique'      => [],
-      'index'       => [ ['_date'], ['level'] ],
+      'primary_key'  => 'lognum',
+      'unique'       => [],
+      'index'        => [ ['_date'], ['level'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                        ],
     },
 
     'log_context' => {
@@ -4284,9 +5896,14 @@ sub tables_hashref {
         'lognum', 'int', '', '', '', '',
         'context', 'varchar', '', 32, '', '',
       ],
-      'primary_key' => 'logcontextnum',
-      'unique' => [ [ 'lognum', 'context' ] ],
-      'index' => [],
+      'primary_key'  => 'logcontextnum',
+      'unique'       => [ [ 'lognum', 'context' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'lognum' ],
+                            table      => 'log',
+                          },
+                        ],
     },
 
     'svc_alarm' => {
@@ -4300,9 +5917,14 @@ sub tables_hashref {
         #cs
         #rep
       ],
-      'primary_key' => 'svcnum',
-      'unique' => [], #system/type/acctnum??
-      'index'  => [],
+      'primary_key'  => 'svcnum',
+      'unique'       => [], #system/type/acctnum??
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                        ],
     },
 
     'svc_cable' => {
@@ -4314,9 +5936,20 @@ sub tables_hashref {
         'serialnum', 'varchar', 'NULL', $char_d, '', '',
         'mac_addr',  'varchar', 'NULL',      12, '', '', 
       ],
-      'primary_key' => 'svcnum',
-      'unique' => [],
-      'index'  => [],
+      'primary_key'  => 'svcnum',
+      'unique'       => [],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'svcnum' ],
+                            table      => 'cust_svc',
+                          },
+                          { columns    => [ 'providernum' ],
+                            table      => 'cable_provider',
+                          },
+                          { columns    => [ 'modelnum' ],
+                            table      => 'cable_model',
+                          },
+                        ],
     },
 
     'cable_model' => {
@@ -4348,9 +5981,14 @@ sub tables_hashref {
         'classnum',     'int',     '',      '', '', '',
         'disabled',    'char', 'NULL',       1, '', '', 
       ],
-      'primary_key' => 'vendnum',
-      'unique'      => [ ['vendname', 'disabled'] ],
-      'index'       => [],
+      'primary_key'  => 'vendnum',
+      'unique'       => [ ['vendname', 'disabled'] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'classnum' ],
+                            table      => 'vend_class',
+                          },
+                        ],
     },
 
     'vend_class' => {
@@ -4371,9 +6009,14 @@ sub tables_hashref {
         '_date',        @date_type,                  '', '', 
         'charged',     @money_type,                  '', '', 
       ],
-      'primary_key' => 'vendbillnum',
-      'unique' => [],
-      'index' => [ ['vendnum'], ['_date'], ],
+      'primary_key'  => 'vendbillnum',
+      'unique'       => [],
+      'index'        => [ ['vendnum'], ['_date'], ],
+      'foreign_keys' => [
+                          { columns    => [ 'vendnum' ],
+                            table      => 'vend_main',
+                          },
+                        ],
     },
 
     'vend_pay' => {
@@ -4383,9 +6026,14 @@ sub tables_hashref {
         '_date',     @date_type,                   '', '', 
         'paid',      @money_type,                  '', '', 
       ],
-      'primary_key' => 'vendpaynum',
-      'unique' => [],
-      'index' => [ [ 'vendnum' ], [ '_date' ], ],
+      'primary_key'  => 'vendpaynum',
+      'unique'       => [],
+      'index'        => [ [ 'vendnum' ], [ '_date' ], ],
+      'foreign_keys' => [
+                          { columns    => [ 'vendnum' ],
+                            table      => 'vend_main',
+                          },
+                        ],
     },
 
     'vend_bill_pay' => {
@@ -4396,9 +6044,17 @@ sub tables_hashref {
         'amount',  @money_type, '', '', 
         #? '_date',   @date_type, '', '', 
       ],
-      'primary_key' => 'vendbillpaynum',
-      'unique' => [],
-      'index' => [ [ 'vendbillnum' ], [ 'vendpaynum' ] ],
+      'primary_key'  => 'vendbillpaynum',
+      'unique'       => [],
+      'index'        => [ [ 'vendbillnum' ], [ 'vendpaynum' ] ],
+      'foreign_keys' => [
+                          { columns    => [ 'vendbillnum' ],
+                            table      => 'vend_bill',
+                          },
+                          { columns    => [ 'vendpaynum' ],
+                            table      => 'vend_pay',
+                          },
+                        ],
     },
 
     %{ tables_hashref_torrus() },
@@ -4421,9 +6077,14 @@ sub tables_hashref {
         'derivenum',       'int', '', '', '', '',
         'serviceid',   'varchar', '', 64, '', '', #srvexport / reportfields
       ],
-      'primary_key' => 'componentnum',
-      'unique'      => [ [ 'derivenum', 'serviceid' ], ],
-      'index'       => [ [ 'derivenum', ], ],
+      'primary_key'  => 'componentnum',
+      'unique'       => [ [ 'derivenum', 'serviceid' ], ],
+      'index'        => [ [ 'derivenum', ], ],
+      'foreign_keys' => [
+                          { columns    => [ 'derivenum' ],
+                            table      => 'torrus_srvderive',
+                          },
+                        ],
     },
 
     'invoice_mode' => {
@@ -4435,6 +6096,11 @@ sub tables_hashref {
       'primary_key' => 'modenum',
       'unique'      => [ ],
       'index'       => [ ],
+      'foreign_keys' => [
+                          { columns    => [ 'agentnum' ],
+                            table      => 'agent',
+                          },
+                        ],
     },
 
     'invoice_conf' => {
@@ -4468,9 +6134,14 @@ sub tables_hashref {
         'logo_eps',             'blob',     'NULL', '', '', '',
         'lpr',                  'varchar',  'NULL', $char_d, '', '',
       ],
-      'primary_key' => 'confnum',
-      'unique' => [ [ 'modenum', 'locale' ] ],
-      'index'  => [ ],
+      'primary_key'  => 'confnum',
+      'unique'       => [ [ 'modenum', 'locale' ] ],
+      'index'        => [ ],
+      'foreign_keys' => [
+                          { columns    => [ 'modenum' ],
+                            table      => 'invoice_mode',
+                          },
+                        ],
     },
 
     # name type nullability length default local
index 037c4b3..a7fe99f 100644 (file)
@@ -132,6 +132,9 @@ sub upgrade {
   local $FS::UID::AutoCommit = 0;
   local $FS::UID::AutoCommit = 0;
 
+  local $FS::cust_pkg::upgrade = 1; #go away after setup+start dates cleaned up for old customers
+
+
   foreach my $table ( keys %$data ) {
 
     my $class = "FS::$table";
index 7c25acb..c938474 100644 (file)
@@ -11,6 +11,7 @@ use FS::access_user_pref;
 use FS::access_usergroup;
 use FS::agent;
 use FS::cust_main;
+use FS::sales;
 
 $DEBUG = 0;
 $me = '[FS::access_user]';
@@ -213,6 +214,7 @@ sub check {
     || $self->ut_textn('last')
     || $self->ut_textn('first')
     || $self->ut_foreign_keyn('user_custnum', 'cust_main', 'custnum')
+    || $self->ut_foreign_keyn('report_salesnum', 'sales', 'salesnum')
     || $self->ut_enum('disabled', [ '', 'Y' ] )
   ;
   return $error if $error;
@@ -246,6 +248,18 @@ sub user_cust_main {
   qsearchs( 'cust_main', { 'custnum' => $self->user_custnum } );
 }
 
+=item report_sales
+
+Returns the FS::sales object (see L<FS::sales>), if any, for this
+user.
+
+=cut
+
+sub report_sales {
+  my $self = shift;
+  qsearchs( 'sales', { 'salesnum' => $self->report_salesnum } );
+}
+
 =item access_usergroup
 
 Returns links to the the groups this user is a part of, as FS::access_usergroup
index 8fcd724..da6f2eb 100644 (file)
@@ -1,7 +1,7 @@
 package FS::contact;
+use base qw( FS::Record );
 
 use strict;
-use base qw( FS::Record );
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::prospect_main;
 use FS::cust_main;
@@ -153,6 +153,16 @@ sub insert {
 
   }
 
+  #unless ( $import || $skip_fuzzyfiles ) {
+    #warn "  queueing fuzzyfiles update\n"
+    #  if $DEBUG > 1;
+    $error = $self->queue_fuzzyfiles_update;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "updating fuzzy search cache: $error";
+    }
+  #}
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   '';
@@ -277,6 +287,16 @@ sub replace {
 
   }
 
+  #unless ( $import || $skip_fuzzyfiles ) {
+    #warn "  queueing fuzzyfiles update\n"
+    #  if $DEBUG > 1;
+    $error = $self->queue_fuzzyfiles_update;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "updating fuzzy search cache: $error";
+    }
+  #}
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   '';
@@ -306,6 +326,44 @@ sub _parse_phonestring {
   );
 }
 
+=item queue_fuzzyfiles_update
+
+Used by insert & replace to update the fuzzy search cache
+
+=cut
+
+use FS::cust_main::Search;
+sub queue_fuzzyfiles_update {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  foreach my $field ( 'first', 'last' ) {
+    my $queue = new FS::queue { 
+      'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
+    };
+    my @args = "contact.$field", $self->get($field);
+    my $error = $queue->insert( @args );
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "queueing job (transaction rolled back): $error";
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
 =item check
 
 Checks all fields to make sure this is a valid example.  If there is
@@ -381,6 +439,11 @@ sub contact_email {
   qsearch('contact_email', { 'contactnum' => $self->contactnum } );
 }
 
+sub cust_main {
+  my $self = shift;
+  qsearchs('cust_main', { 'custnum' => $self->custnum  } );
+}
+
 =back
 
 =head1 BUGS
index 1276d8d..4f78735 100644 (file)
@@ -1,8 +1,9 @@
 package FS::contact_email;
+use base qw( FS::Record );
 
 use strict;
-use base qw( FS::Record );
 use FS::Record qw( qsearch qsearchs );
+use FS::contact;
 
 =head1 NAME
 
@@ -25,8 +26,9 @@ FS::contact_email - Object methods for contact_email records
 
 =head1 DESCRIPTION
 
-An FS::contact_email object represents an example.  FS::contact_email inherits from
-FS::Record.  The following fields are currently supported:
+An FS::contact_email object represents a contact's email address.
+FS::contact_email inherits from FS::Record.  The following fields are currently
+supported:
 
 =over 4
 
@@ -51,15 +53,14 @@ emailaddress
 
 =item new HASHREF
 
-Creates a new example.  To add the example to the database, see L<"insert">.
+Creates a new contact email address.  To add the email address to the database,
+see L<"insert">.
 
 Note that this stores the hash reference, not a distinct copy of the hash it
 points to.  You can ask the object for a copy with the I<hash> method.
 
 =cut
 
-# the new method can be inherited from FS::Record, if a table method is defined
-
 sub table { 'contact_email'; }
 
 =item insert
@@ -67,60 +68,62 @@ sub table { 'contact_email'; }
 Adds this record to the database.  If there is an error, returns the error,
 otherwise returns false.
 
-=cut
-
-# the insert method can be inherited from FS::Record
-
 =item delete
 
 Delete this record from the database.
 
-=cut
-
-# the delete method can be inherited from FS::Record
-
 =item replace OLD_RECORD
 
 Replaces the OLD_RECORD with this one in the database.  If there is an error,
 returns the error, otherwise returns false.
 
-=cut
-
-# the replace method can be inherited from FS::Record
-
 =item check
 
-Checks all fields to make sure this is a valid example.  If there is
+Checks all fields to make sure this is a valid email address.  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('contactemailnum')
     || $self->ut_number('contactnum')
-    || $self->ut_text('emailaddress')
   ;
   return $error if $error;
 
+  #technically \w and also ! # $ % & ' * + - / = ? ^ _ ` { | } ~
+  # and even more technically need to deal with i18n addreesses soon
+  #  (maybe the UI can convert them for us ala punycode.js)
+  # but for now in practice have not encountered anything outside \w . - & + '
+  #  and even & and ' are super rare and probably have scarier "pass to shell"
+  #   implications than worth being pedantic about accepting
+  #    (we always String::ShellQuote quote them, but once passed...)
+  #                              SO: \w . - +
+  if ( $self->emailaddress =~ /^\s*([\w\.\-\+]+)\@(([\w\.\-]+\.)+\w+)\s*$/ ) {
+    my($user, $domain) = ($1, $2);
+    $self->emailaddress("$1\@$2");
+  } else {
+    return gettext("illegal_email_invoice_address"). ': '. $self->emailaddress;
+  }
+
   $self->SUPER::check;
 }
 
+sub contact {
+  my $self = shift;
+  qsearchs( 'contact', { 'contactnum' => $self->contactnum } );
+}
+
 =back
 
 =head1 BUGS
 
-The author forgot to customize this manpage.
-
 =head1 SEE ALSO
 
-L<FS::Record>, schema.html from the base documentation.
+L<FS::contact>, L<FS::Record>
 
 =cut
 
index ad8e8f7..0eb2166 100644 (file)
@@ -1,8 +1,9 @@
 package FS::contact_phone;
+use base qw( FS::Record );
 
 use strict;
-use base qw( FS::Record );
 use FS::Record qw( qsearch qsearchs );
+use FS::contact;
 
 =head1 NAME
 
@@ -25,8 +26,8 @@ FS::contact_phone - Object methods for contact_phone records
 
 =head1 DESCRIPTION
 
-An FS::contact_phone object represents an example.  FS::contact_phone inherits from
-FS::Record.  The following fields are currently supported:
+An FS::contact_phone object represents a contatct's phone number.
+FS::contact_phone inherits from FS::Record.  The following fields are currently supported:
 
 =over 4
 
@@ -63,15 +64,14 @@ extension
 
 =item new HASHREF
 
-Creates a new example.  To add the example to the database, see L<"insert">.
+Creates a new phone number.  To add the phone number to the database, see
+L<"insert">.
 
 Note that this stores the hash reference, not a distinct copy of the hash it
 points to.  You can ask the object for a copy with the I<hash> method.
 
 =cut
 
-# the new method can be inherited from FS::Record, if a table method is defined
-
 sub table { 'contact_phone'; }
 
 =item insert
@@ -79,38 +79,23 @@ sub table { 'contact_phone'; }
 Adds this record to the database.  If there is an error, returns the error,
 otherwise returns false.
 
-=cut
-
-# the insert method can be inherited from FS::Record
-
 =item delete
 
 Delete this record from the database.
 
-=cut
-
-# the delete method can be inherited from FS::Record
-
 =item replace OLD_RECORD
 
 Replaces the OLD_RECORD with this one in the database.  If there is an error,
 returns the error, otherwise returns false.
 
-=cut
-
-# the replace method can be inherited from FS::Record
-
 =item check
 
-Checks all fields to make sure this is a valid example.  If there is
+Checks all fields to make sure this is a valid phone number.  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;
 
@@ -124,18 +109,48 @@ sub check {
   ;
   return $error if $error;
 
+  #strip non-digits, UI should format numbers per countrycode
+  (my $phonenum = $self->phonenum ) =~ s/\D//g;
+  $self->phonenum($phonenum);
+
   $self->SUPER::check;
 }
 
+sub phonenum_pretty {
+  my $self = shift;
+
+  #until/unless we have the upgrade strip all whitespace
+  (my $phonenum = $self->phonenum ) =~ s/\D//g;
+
+  if ( $self->countrycode == 1 ) {
+
+    $phonenum =~ /^(\d{3})(\d{3})(\d{4})(\d*)$/
+      or return $self->phonenum; #wtf?
+
+    $phonenum = "($1) $2-$3";
+    $phonenum .= " x$4" if $4;
+    return $phonenum;
+
+  } else {
+    warn "don't know how to format phone numbers for country +". $self->countrycode;
+    #also, the UI doesn't have a good way for you to enter them yet or parse a countrycode from the number
+    return $self->phonenum;
+  }
+
+}
+
+sub contact {
+  my $self = shift;
+  qsearchs( 'contact', { 'contactnum' => $self->contactnum } );
+}
+
 =back
 
 =head1 BUGS
 
-The author forgot to customize this manpage.
-
 =head1 SEE ALSO
 
-L<FS::Record>, schema.html from the base documentation.
+L<FS::contact>, L<FS::Record>
 
 =cut
 
index 66d98c2..4e34ef4 100644 (file)
@@ -3070,7 +3070,8 @@ sub _items_payments {
     my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
     my $desc = $self->mt('Payment received').' '.
                time2str($date_format, $cust_pay->_date );
-    $desc .= $self->mt(' via ' . $cust_pay->payby_payinfo_pretty)
+    $desc .= $self->mt(' via ') .
+             $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
       if $detailed;
 
     push @b, {
index 9678934..c459d82 100644 (file)
@@ -22,6 +22,7 @@ use FS::cust_event;
 use FS::agent;
 use FS::sales;
 use FS::cust_credit_void;
+use FS::upgrade_journal;
 
 $me = '[ FS::cust_credit ]';
 $DEBUG = 0;
@@ -620,6 +621,100 @@ sub _upgrade_data {  # class method
   local($ignore_empty_reasonnum) = 1;
   $class->_upgrade_otaker(%opts);
 
+  if ( !FS::upgrade_journal->is_done('cust_credit__tax_link')
+      and !$conf->exists('enable_taxproducts') ) {
+    # RT#25458: fix credit line item applications that should refer to a 
+    # specific tax allocation
+    my @cust_credit_bill_pkg = qsearch({
+        table     => 'cust_credit_bill_pkg',
+        select    => 'cust_credit_bill_pkg.*',
+        addl_from => ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
+        extra_sql =>
+          'WHERE cust_credit_bill_pkg.billpkgtaxlocationnum IS NULL '.
+          'AND cust_bill_pkg.pkgnum = 0', # is a tax
+    });
+    my %tax_items;
+    my %credits;
+    foreach (@cust_credit_bill_pkg) {
+      my $billpkgnum = $_->billpkgnum;
+      $tax_items{$billpkgnum} ||= FS::cust_bill_pkg->by_key($billpkgnum);
+      $credits{$billpkgnum} ||= [];
+      push @{ $credits{$billpkgnum} }, $_;
+    }
+    TAX_ITEM: foreach my $tax_item (values %tax_items) {
+      my $billpkgnum = $tax_item->billpkgnum;
+      # get all pkg/location/taxrate allocations of this tax line item
+      my @allocations = sort {$b->amount <=> $a->amount}
+                        qsearch('cust_bill_pkg_tax_location', {
+                            billpkgnum => $billpkgnum
+                        });
+      # and these are all credit applications to it
+      my @credits = sort {$b->amount <=> $a->amount}
+                    @{ $credits{$billpkgnum} };
+      my $c = shift @credits;
+      my $a = shift @allocations; # we will NOT modify these
+      while ($c and $a) {
+        if ( abs($c->amount - $a->amount) < 0.005 ) {
+          # by far the most common case: the tax line item is for a single
+          # tax, so we just fill in the billpkgtaxlocationnum
+          $c->set('billpkgtaxlocationnum', $a->billpkgtaxlocationnum);
+          my $error = $c->replace;
+          if ($error) {
+            warn "error fixing credit application to tax item #$billpkgnum:\n$error\n";
+            next TAX_ITEM;
+          }
+          $c = shift @credits;
+          $a = shift @allocations;
+        } elsif ( $c->amount > $a->amount ) {
+          # fairly common: the tax line contains tax for multiple packages
+          # (or multiple taxes) but the credit isn't divided up
+          my $new_link = FS::cust_credit_bill_pkg->new({
+              creditbillnum         => $c->creditbillnum,
+              billpkgnum            => $c->billpkgnum,
+              billpkgtaxlocationnum => $a->billpkgtaxlocationnum,
+              amount                => $a->amount,
+              setuprecur            => 'setup',
+          });
+          my $error = $new_link->insert;
+          if ($error) {
+            warn "error fixing credit application to tax item #$billpkgnum:\n$error\n";
+            next TAX_ITEM;
+          }
+          $c->set(amount => sprintf('%.2f', $c->amount - $a->amount));
+          $a = shift @allocations;
+        } elsif ( $c->amount < 0.005 ) {
+          # also fairly common; we can delete these with no harm
+          my $error = $c->delete;
+          warn "error removing zero-amount credit application (probably harmless):\n$error\n" if $error;
+          $c = shift @credits;
+        } elsif ( $c->amount < $a->amount ) {
+          # should never happen, but if it does, handle it gracefully
+          $c->set('billpkgtaxlocationnum', $a->billpkgtaxlocationnum);
+          my $error = $c->replace;
+          if ($error) {
+            warn "error fixing credit application to tax item #$billpkgnum:\n$error\n";
+            next TAX_ITEM;
+          }
+          $a->set(amount => $a->amount - $c->amount);
+          $c = shift @credits;
+        }
+      } # while $c and $a
+      if ( $c ) {
+        if ( $c->amount < 0.005 ) {
+          my $error = $c->delete;
+          warn "error removing zero-amount credit application (probably harmless):\n$error\n" if $error;
+        } elsif ( $c->modified ) {
+          # then we've allocated part of it, so reduce the nonspecific 
+          # application by that much
+          my $error = $c->replace;
+          warn "error fixing credit application to tax item #$billpkgnum:\n$error\n" if $error;
+        }
+        # else there are probably no allocations, i.e. this is a pre-3.x 
+        # record that was never migrated over, so leave it alone
+      } # if $c
+    } # foreach $tax_item
+    FS::upgrade_journal->set_done('cust_credit__tax_link');
+  }
 }
 
 =back
@@ -902,7 +997,7 @@ sub credit_lineitems {
       {
         # the existing tax_Xlocation object
         my $old_loc =
-          $tax_links{$tax_item->billpkgnum}{$new_loc->taxable_billpkgnum};
+          $tax_links{$tax_item->billpkgnum}{$new_loc->taxable_cust_bill_pkg->billpkgnum};
 
         next if !$old_loc; # apply the leftover amount nonspecifically
 
index 3e36c60..d768f84 100644 (file)
@@ -1662,13 +1662,25 @@ sub queue_fuzzyfiles_update {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
+  foreach my $field ( 'first', 'last', 'company' ) {
+    my $queue = new FS::queue { 
+      'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
+    };
+    my @args = "cust_main.$field", $self->get($field);
+    my $error = $queue->insert( @args );
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "queueing job (transaction rolled back): $error";
+    }
+  }
+
   my @locations = $self->bill_location;
   push @locations, $self->ship_location if $self->has_ship_address;
   foreach my $location (@locations) {
     my $queue = new FS::queue { 
-      'job' => 'FS::cust_main::Search::append_fuzzyfiles'
+      'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
     };
-    my @args = map $location->get($_), @FS::cust_main::Search::fuzzyfields;
+    my @args = 'cust_location.address1', $location->address1;
     my $error = $queue->insert( @args );
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
index 0a364f5..b8a71d4 100644 (file)
@@ -373,6 +373,11 @@ sub bill {
   my $time = $options{'time'} || time;
   my $invoice_time = $options{'invoice_time'} || $time;
 
+  my $cmp_time = ( $conf->exists('next-bill-ignore-time')
+                     ? day_end( $time )
+                     : $time
+                 );
+
   $options{'not_pkgpart'} ||= {};
   $options{'not_pkgpart'} = { map { $_ => 1 }
                                   split(/\s*,\s*/, $options{'not_pkgpart'})
@@ -480,7 +485,7 @@ sub bill {
       my $next_bill = $cust_pkg->getfield('bill') || 0;
       my $error;
       # let this run once if this is the last bill upon cancellation
-      while ( $next_bill <= $time or $options{cancel} ) {
+      while ( $next_bill <= $cmp_time or $options{cancel} ) {
         $error =
           $self->_make_lines( 'part_pkg'            => $part_pkg,
                               'cust_pkg'            => $cust_pkg,
index 5590f88..52fe313 100644 (file)
@@ -165,7 +165,10 @@ sub _upgrade_data {
         map { $_ => $cust_main->get($_) } location_fields(),
       }
     );
-    $bill_location->set('censustract', ''); # properly goes with ship_location
+    $bill_location->set('censustract', '');
+    $bill_location->set('censusyear', '');
+     # properly goes with ship_location; if they're the same, will be set
+     # on ship_location before inserting either one
     my $ship_location = $bill_location; # until proven otherwise
 
     if ( $cust_main->get('ship_address1') ) {
@@ -187,8 +190,6 @@ sub _upgrade_data {
         );
       } # else it stays equal to $bill_location
 
-      $ship_location->set('censustract', $cust_main->get('censustract'));
-
       # Step 2: Extract shipping address contact fields into contact
       my %unlike = map { $_ => 1 }
         grep { $cust_main->get($_) ne $cust_main->get("ship_$_") }
@@ -251,6 +252,11 @@ sub _upgrade_data {
       }
     }
 
+    # this always goes with the ship_location (whether it's the same as
+    # bill_location or not)
+    $ship_location->set('censustract', $cust_main->get('censustract'));
+    $ship_location->set('censusyear',  $cust_main->get('censusyear'));
+
     $error = $bill_location->insert;
     die "error migrating billing address for customer $custnum: $error"
       if $error;
@@ -286,6 +292,38 @@ sub _upgrade_data {
     }
 
   } #foreach $cust_main
+
+  # repair an error in earlier upgrades
+  if (!FS::upgrade_journal->is_done('cust_location_censustract_repair')
+       and FS::Conf->new->exists('cust_main-require_censustract') ) {
+
+    foreach my $cust_location (
+      qsearch('cust_location', { 'censustract' => '' })
+    ) {
+      my $custnum = $cust_location->custnum;
+      next if !$custnum; # avoid doing this for prospect locations
+      my $address1 = $cust_location->address1;
+      # find the last history record that had that address
+      my $last_h = qsearchs({
+          table     => 'h_cust_main',
+          extra_sql => " WHERE custnum = $custnum AND address1 = ".
+                        dbh->quote($address1) .
+                        " AND censustract IS NOT NULL",
+          order_by  => " ORDER BY history_date DESC LIMIT 1",
+      });
+      if (!$last_h) {
+        # this is normal; just means it never had a census tract before
+        next;
+      }
+      $cust_location->set('censustract' => $last_h->get('censustract'));
+      $cust_location->set('censusyear'  => $last_h->get('censusyear'));
+      my $error = $cust_location->replace;
+      warn "Error setting census tract for customer #$custnum:\n  $error\n"
+        if $error;
+    } # foreach $cust_location
+    FS::upgrade_journal->set_done('cust_location_censustract_repair');
+  }
+
 }
 
 =back
index 182527f..16db712 100644 (file)
@@ -19,8 +19,11 @@ use FS::payinfo_Mixin;
 $DEBUG = 0;
 $me = '[FS::cust_main::Search]';
 
-@fuzzyfields = ( 'cust_main.first', 'cust_main.last', 'cust_main.company', 
-  'cust_location.address1' );
+@fuzzyfields = (
+  'cust_main.first', 'cust_main.last', 'cust_main.company', 
+  'cust_location.address1',
+  'contact.first',   'contact.last',
+);
 
 install_callback FS::UID sub { 
   $conf = new FS::Conf;
@@ -72,6 +75,7 @@ sub smart_search {
   #here is the agent virtualization
   my $agentnums_sql = 
     $FS::CurrentUser::CurrentUser->agentnums_sql(table => 'cust_main');
+  my $agentnums_href = $FS::CurrentUser::CurrentUser->agentnums_href;
 
   my @cust_main = ();
 
@@ -85,6 +89,10 @@ sub smart_search {
     my $phonen = "$1-$2-$3";
     $phonen .= " x$4" if $4;
 
+    my $phonenum = "$1$2$3";
+    #my $extension = $4;
+
+    #cust_main phone numbers
     push @cust_main, qsearch( {
       'table'   => 'cust_main',
       'hashref' => { %options },
@@ -97,6 +105,16 @@ sub smart_search {
                      " AND $agentnums_sql", #agent virtualization
     } );
 
+    #contact phone numbers
+    push @cust_main,
+      grep $agentnums_href->{$_->agentnum}, #agent virt
+        grep $_, #skip contacts that don't have cust_main records
+          map $_->contact->cust_main,
+            qsearch({
+                      'table'   => 'contact_phone',
+                      'hashref' => { 'phonenum' => $phonenum },
+                   });
+
     unless ( @cust_main || $phonen =~ /x\d+$/ ) { #no exact match
       #try looking for matches with extensions unless one was specified
 
@@ -117,8 +135,11 @@ sub smart_search {
   } 
   
   
-  if ( $search =~ /@/ ) { #invoicing email address
+  if ( $search =~ /@/ ) { #email address
+
+      # invoicing email address
       push @cust_main,
+        grep $agentnums_href->{$_->agentnum}, #agent virt
          map $_->cust_main,
              qsearch( {
                         'table'     => 'cust_main_invoice',
@@ -126,6 +147,17 @@ sub smart_search {
                       }
                     );
 
+      # contact email address
+      push @cust_main,
+        grep $agentnums_href->{$_->agentnum}, #agent virt
+          grep $_, #skip contacts that don't have cust_main records
+           map $_->contact->cust_main,
+             qsearch( {
+                        'table'     => 'contact_email',
+                        'hashref'   => { 'emailaddress' => $search },
+                      }
+                    );
+
   # custnum search (also try agent_custid), with some tweaking options if your
   # legacy cust "numbers" have letters
   } elsif ( $search =~ /^\s*(\d+)\s*$/
@@ -159,7 +191,7 @@ sub smart_search {
     # for all agents this user can see, if any of them have custnum prefixes 
     # that match the search string, include customers that match the rest 
     # of the custnum and belong to that agent
-    foreach my $agentnum ( $FS::CurrentUser::CurrentUser->agentnums ) {
+    foreach my $agentnum ( keys %$agentnums_href ) {
       my $p = $conf->config('cust_main-custnum-display_prefix', $agentnum);
       next if !$p;
       if ( $p eq substr($num, 0, length($p)) ) {
@@ -216,10 +248,12 @@ sub smart_search {
           $agentnums_sql,
         ),
       } ),
+
     #contacts?
+    # probably not necessary for the "something a browser remembered" case
 
   } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { # value search
-                                              # try (ship_){last,company}
+                                              # try {first,last,company}
 
     my $value = lc($1);
 
@@ -256,12 +290,25 @@ sub smart_search {
       my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
       $sql .= "( LOWER(cust_main.last) = $q_last AND LOWER(cust_main.first) = $q_first )";
 
+      #cust_main
       push @cust_main, qsearch( {
         'table'     => 'cust_main',
         'hashref'   => \%options,
         'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
       } );
-      #contacts?
+
+      #contacts
+      push @cust_main,
+        grep $agentnums_href->{$_->agentnum}, #agent virt
+          grep $_, #skip contacts that don't have cust_main records
+           map $_->cust_main,
+             qsearch( {
+                        'table'     => 'contact',
+                        'hashref'   => { 'first' => $first,
+                                          'last'  => $last,
+                                        }, 
+                      }
+                    );
 
       # or it just be something that was typed in... (try that in a sec)
 
@@ -271,18 +318,28 @@ sub smart_search {
 
     #exact
     my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
-    $sql .= " (    LOWER(last)          = $q_value
-                OR LOWER(company)       = $q_value
+    $sql .= " (    LOWER(cust_main.first)         = $q_value
+                OR LOWER(cust_main.last)          = $q_value
+                OR LOWER(cust_main.company)       = $q_value
             ";
-    #yes, it's a kludge
-    $sql .= "   OR EXISTS( 
-                SELECT 1 FROM cust_location 
-                WHERE LOWER(cust_location.address1) = $q_value
-                  AND cust_location.custnum = cust_main.custnum
-            )
-            "
+
+    #address1 (yes, it's a kludge)
+    $sql .= "   OR EXISTS ( 
+                            SELECT 1 FROM cust_location 
+                              WHERE LOWER(cust_location.address1) = $q_value
+                                AND cust_location.custnum = cust_main.custnum
+                          )"
       if $conf->exists('address1-search');
-    $sql .= " )";
+
+    #contacts (look, another kludge)
+    $sql .= "   OR EXISTS ( SELECT 1 FROM contact
+                              WHERE (    LOWER(contact.first) = $q_value
+                                      OR LOWER(contact.last)  = $q_value
+                                    )
+                                AND contact.custnum IS NOT NULL
+                                AND contact.custnum = cust_main.custnum
+                          )
+              ) ";
 
     push @cust_main, qsearch( {
       'table'     => 'cust_main',
@@ -304,7 +361,6 @@ sub smart_search {
       );
 
       if ( $first && $last ) {
-        #contacts? ship_first/ship_last are gone
 
         push @hashrefs,
           { 'first'        => { op=>'ILIKE', value=>"%$first%" },
@@ -315,6 +371,7 @@ sub smart_search {
       } else {
 
         push @hashrefs,
+          { 'first'        => { op=>'ILIKE', value=>"%$value%" }, },
           { 'last'         => { op=>'ILIKE', value=>"%$value%" }, },
         ;
       }
@@ -334,14 +391,35 @@ sub smart_search {
       if ( $conf->exists('address1-search') ) {
 
         push @cust_main, qsearch( {
-          'table'     => 'cust_main',
-          'addl_from' => 'JOIN cust_location USING (custnum)',
-          'extra_sql' => 'WHERE cust_location.address1 ILIKE '.
-                          dbh->quote("%$value%"),
+          table     => 'cust_main',
+          addl_from => 'JOIN cust_location USING (custnum)',
+          extra_sql => 'WHERE '.
+                        ' cust_location.address1 ILIKE '.dbh->quote("%$value%").
+                        " AND $agentnums_sql", #agent virtualizaiton
         } );
 
       }
 
+      #contact substring
+
+      shift @hashrefs; #no company column in contact table
+     
+      foreach my $hashref ( @hashrefs ) {
+
+        push @cust_main,
+          grep $agentnums_href->{$_->agentnum}, #agent virt
+            grep $_, #skip contacts that don't have cust_main records
+             map $_->cust_main,
+                qsearch({
+                          'table'     => 'contact',
+                          'hashref'   => { %$hashref,
+                                           #%options,
+                                         },
+                          #'extra_sql' => " AND $agentnums_sql", #agent virt
+                       });
+
+      }
+
       #fuzzy
       my %fuzopts = (
         'hashref'   => \%options,
@@ -355,15 +433,30 @@ sub smart_search {
             'first'  => $first }, #
           %fuzopts
         );
+        push @cust_main, FS::cust_main::Search->fuzzy_search(
+          { 'contact.last'   => $last,    #fuzzy hashref
+            'contact.first'  => $first }, #
+          %fuzopts
+        );
+     }
+      foreach my $field ( 'first', 'last', 'company' ) {
+        push @cust_main, FS::cust_main::Search->fuzzy_search(
+          { $field => $value },
+          %fuzopts
+        );
       }
-      foreach my $field ( 'last', 'company' ) {
-        push @cust_main,
-          FS::cust_main::Search->fuzzy_search( { $field => $value }, %fuzopts );
+      foreach my $field ( 'first', 'last' ) {
+        push @cust_main, FS::cust_main::Search->fuzzy_search(
+          { "contact.$field" => $value },
+          %fuzopts
+        );
       }
       if ( $conf->exists('address1-search') ) {
         push @cust_main,
           FS::cust_main::Search->fuzzy_search(
-            { 'cust_location.address1' => $value }, %fuzopts );
+            { 'cust_location.address1' => $value },
+            %fuzopts
+        );
       }
 
     }
@@ -668,22 +761,6 @@ sub search {
     unless $params->{'cancelled_pkgs'};
 
   ##
-  # parse without census tract checkbox
-  ##
-
-  push @where, "(ship_location.censustract = '' or ship_location.censustract is null)"
-    if $params->{'no_censustract'};
-
-  ##
-  # parse with hardcoded tax location checkbox
-  ##
-
-  my $tax_prefix = FS::Conf->new->exists('tax-ship_location') ? 'ship_' 
-                                                              : 'bill_';
-  push @where, "${tax_prefix}location.geocode is not null"
-    if $params->{'with_geocode'};
-
-  ##
   # "with email address(es)" checkbox
   ##
 
@@ -950,19 +1027,6 @@ sub search {
 
   }
 
-  if ( $params->{'with_geocode'} ) {
-
-    unshift @extra_headers, 'Tax location override', 'Calculated tax location';
-    unshift @extra_fields, sub { my $c = shift; $c->get('geocode'); },
-                           sub { my $c = shift;
-                                 $c->set('geocode', '');
-                                 $c->geocode('cch'); #XXX only cch right now
-                               };
-    push @select, 'geocode';
-    push @select, 'zip' unless grep { $_ eq 'zip' } @select;
-    push @select, 'ship_zip' unless grep { $_ eq 'ship_zip' } @select;
-  }
-
   my $select = join(', ', @select);
 
   my $sql_query = {
@@ -976,7 +1040,7 @@ sub search {
     'extra_headers' => \@extra_headers,
     'extra_fields'  => \@extra_fields,
   };
-  warn Data::Dumper::Dumper($sql_query);
+  #warn Data::Dumper::Dumper($sql_query);
   $sql_query;
 
 }
@@ -1033,8 +1097,10 @@ sub fuzzy_search {
     $extra_sql .= "$field $in_matches";
 
     my $addl_from = $fuzopts{addl_from};
-    if ( $field =~ /^cust_location/ ) {
+    if ( $field =~ /^cust_location\./ ) {
       $addl_from .= ' JOIN cust_location USING (custnum)';
+    } elsif ( $field =~ /^contact\./ ) {
+      $addl_from .= ' JOIN contact USING (custnum)';
     }
 
     push @cust_main, qsearch({
@@ -1064,7 +1130,14 @@ sub fuzzy_search {
 
 sub check_and_rebuild_fuzzyfiles {
   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
-  rebuild_fuzzyfiles() if grep { ! -e "$dir/cust_main.$_" } @fuzzyfields;
+  rebuild_fuzzyfiles()
+    if grep { ! -e "$dir/$_" }
+         map {
+               my ($field, $table) = reverse split('\.', $_);
+               $table ||= 'cust_main';
+               "$table.$field"
+             }
+           @fuzzyfields;
 }
 
 =item rebuild_fuzzyfiles
@@ -1117,34 +1190,47 @@ sub append_fuzzyfiles {
 
   check_and_rebuild_fuzzyfiles();
 
-  use Fcntl qw(:flock);
+  #foreach my $fuzzy (@fuzzyfields) {
+  foreach my $fuzzy ( 'cust_main.first', 'cust_main.last', 'cust_main.company', 
+                      'cust_location.address1',
+                    ) {
 
-  my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+    append_fuzzyfiles_fuzzyfield($fuzzy, shift);
 
-  foreach my $fuzzy (@fuzzyfields) {
+  }
 
-    my ($field, $table) = reverse split('\.', $fuzzy);
-    $table ||= 'cust_main';
+  1;
+}
 
-    my $value = shift;
+=item append_fuzzyfiles_fuzzyfield COLUMN VALUE
 
-    if ( $value ) {
+=item append_fuzzyfiles_fuzzyfield TABLE.COLUMN VALUE
 
-      open(CACHE, '>>:encoding(UTF-8)', "$dir/$table.$field" )
-        or die "can't open $dir/$table.$field: $!";
-      flock(CACHE,LOCK_EX)
-        or die "can't lock $dir/$table.$field: $!";
+=cut
 
-      print CACHE "$value\n";
+use Fcntl qw(:flock);
+sub append_fuzzyfiles_fuzzyfield {
+  my( $fuzzyfield, $value ) = @_;
 
-      flock(CACHE,LOCK_UN)
-        or die "can't unlock $dir/$table.$field: $!";
-      close CACHE;
-    }
+  my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
 
-  }
 
-  1;
+  my ($field, $table) = reverse split('\.', $fuzzyfield);
+  $table ||= 'cust_main';
+
+  return unless length($value);
+
+  open(CACHE, '>>:encoding(UTF-8)', "$dir/$table.$field" )
+    or die "can't open $dir/$table.$field: $!";
+  flock(CACHE,LOCK_EX)
+    or die "can't lock $dir/$table.$field: $!";
+
+  print CACHE "$value\n";
+
+  flock(CACHE,LOCK_UN)
+    or die "can't unlock $dir/$table.$field: $!";
+  close CACHE;
+
 }
 
 =item all_X
index eb6a714..5abdbe2 100644 (file)
@@ -50,6 +50,8 @@ use FS::Conf;
 
 our ($disable_agentcheck, $DEBUG, $me, $import) = (0, 0, '[FS::cust_pkg]', 0);
 
+our $upgrade = 0; #go away after setup+start dates cleaned up for old customers
+
 sub _cache {
   my $self = shift;
   my ( $hashref, $cache ) = @_;
@@ -655,7 +657,7 @@ sub check {
   return $error if $error;
 
   return "A package with both start date (future start) and setup date (already started) will never bill"
-    if $self->start_date && $self->setup;
+    if $self->start_date && $self->setup && ! $upgrade;
 
   return "A future unsuspend date can only be set for a package with a suspend date"
     if $self->resume and !$self->susp and !$self->adjourn;
@@ -4282,6 +4284,32 @@ boolean; if true, returns only packages with more than 0 FCC phone lines.
 Limit to packages with a service location in the specified state and country.
 For FCC 477 reporting, mostly.
 
+=item location_cust
+
+Limit to packages whose service locations are the same as the customer's 
+default service location.
+
+=item location_nocust
+
+Limit to packages whose service locations are not the customer's default 
+service location.
+
+=item location_census
+
+Limit to packages whose service locations have census tracts.
+
+=item location_nocensus
+
+Limit to packages whose service locations do not have a census tract.
+
+=item location_geocode
+
+Limit to packages whose locations have geocodes.
+
+=item location_geocode
+
+Limit to packages whose locations do not have geocodes.
+
 =back
 
 =cut
@@ -4514,6 +4542,22 @@ sub search {
   }
 
   ###
+  # location_* flags
+  ###
+  if ( $params->{location_cust} xor $params->{location_nocust} ) {
+    my $op = $params->{location_cust} ? '=' : '!=';
+    push @where, "cust_location.locationnum $op cust_main.ship_locationnum";
+  }
+  if ( $params->{location_census} xor $params->{location_nocensus} ) {
+    my $op = $params->{location_census} ? "IS NOT NULL" : "IS NULL";
+    push @where, "cust_location.censustract $op";
+  }
+  if ( $params->{location_geocode} xor $params->{location_nogeocode} ) {
+    my $op = $params->{location_geocode} ? "IS NOT NULL" : "IS NULL";
+    push @where, "cust_location.geocode $op";
+  }
+
+  ###
   # parse part_pkg
   ###
 
index f6f9945..e66d78c 100644 (file)
@@ -1,8 +1,9 @@
 package FS::discount;
+use base qw( FS::Record );
 
 use strict;
-use base qw( FS::Record );
 use FS::Record qw( qsearch qsearchs );
+use FS::discount_class;
 
 =head1 NAME
 
@@ -130,6 +131,7 @@ sub check {
 
   my $error = 
     $self->ut_numbern('discountnum')
+    || $self->ut_foreign_keyn('classnum', 'discount_class', 'classnum')
     || $self->ut_textn('name')
     || $self->ut_money('amount')
     || $self->ut_float('percent') #actually decimal, but this will do
@@ -140,16 +142,19 @@ sub check {
   ;
   return $error if $error;
 
-  #discourage non-integer months for package discounts
-  if ($self->discountnum) {
-    my $sql =
-      "SELECT count(*) FROM part_pkg_discount WHERE part_pkg_discount.discountnum = ".
-      $self->discountnum;
-
-    my $count = $self->scalar_sql($sql); 
-    return "months must be integers greater than 1"
-      if ( $count && ($self->ut_number('months') || $self->months < 2) );
-  }
+#causes "months must be integers greater than 1" errors when you go back and
+# try to edit an existing discount (because the months format as NN.000)
+#not worth whatever reason it came in with "prepayment discounts rt#5318" for
+#  #discourage non-integer months for package discounts
+#  if ($self->discountnum) {
+#    my $sql =
+#      "SELECT count(*) FROM part_pkg_discount WHERE part_pkg_discount.discountnum = ".
+#      $self->discountnum;
+#
+#    my $count = $self->scalar_sql($sql); 
+#    return "months must be integers greater than 1"
+#      if ( $count && ($self->ut_number('months') || $self->months < 2) );
+#  }
     
   $self->SUPER::check;
 }
@@ -195,6 +200,18 @@ sub description {
   $desc;
 }
 
+sub classname {
+  my $self = shift;
+  my $discount_class = $self->discount_class;
+  $discount_class ? $discount_class->classname : '(none)';
+}
+
+sub discount_class {
+  my $self = shift;
+  qsearchs('discount_class', { 'classnum' => $self->classnum });
+}
+
+
 =back
 
 =head1 BUGS
diff --git a/FS/FS/discount_class.pm b/FS/FS/discount_class.pm
new file mode 100644 (file)
index 0000000..f5b8769
--- /dev/null
@@ -0,0 +1,109 @@
+package FS::discount_class;
+use base qw( FS::class_Common );
+
+use strict;
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::discount_class - Object methods for discount_class records
+
+=head1 SYNOPSIS
+
+  use FS::discount_class;
+
+  $record = new FS::discount_class \%hash;
+  $record = new FS::discount_class { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::discount_class object represents a discount class.  FS::discount_class
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item classnum
+
+primary key
+
+=item classname
+
+classname
+
+=item disabled
+
+disabled
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new discount class.  To add the discount class to the database, see
+L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'discount_class'; }
+
+=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 discount class.  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('classnum')
+    || $self->ut_text('classname')
+    || $self->ut_enum('disabled', [ '', 'Y' ])
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::discount>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_pkg/prorate_calendar.pm b/FS/FS/part_pkg/prorate_calendar.pm
new file mode 100644 (file)
index 0000000..83a80f5
--- /dev/null
@@ -0,0 +1,223 @@
+package FS::part_pkg::prorate_calendar;
+
+use strict;
+use vars qw(@ISA %info);
+use DateTime;
+use Tie::IxHash;
+use base 'FS::part_pkg::flat';
+
+# weird stuff in here
+
+%info = (
+  'name' => 'Prorate to specific calendar day(s), then flat-rate',
+  'shortname' => 'Prorate (calendar cycle)',
+  'inherit_fields' => [ 'flat', 'usage_Mixin', 'global_Mixin' ],
+  'fields' => {
+    'recur_temporality' => {'disabled' => 1},
+    'sync_bill_date' => {'disabled' => 1},# god help us all
+
+    'cutoff_day' => { 'name' => 'Billing day (1 - end of cycle)',
+                      'default' => 1,
+                    },
+
+    # add_full_period is not allowed
+
+    # prorate_round_day is always on
+    'prorate_round_day' => { 'disabled' => 1 },
+    'prorate_defer_bill'=> {
+                        'name' => 'Defer the first bill until the billing day',
+                        'type' => 'checkbox',
+                        },
+    'prorate_verbose' => {
+                        'name' => 'Show prorate details on the invoice',
+                        'type' => 'checkbox',
+                        },
+  },
+  'fieldorder' => [ 'cutoff_day', 'prorate_defer_bill', 'prorate_round_day', 'prorate_verbose' ],
+  'freq' => 'm',
+  'weight' => 20,
+);
+
+my %freq_max_days = ( # the length of the shortest period of each cycle type
+  '1'   => 28,
+  '2'   => 59,   # Jan - Feb
+  '3'   => 90,   # Jan - Mar
+  '4'   => 120,  # Jan - Apr
+  '6'   => 181,  # Jan - Jun
+  '12'  => 365,
+);
+
+my %freq_cutoff_days = (
+  '1'   => [ 31, 28, 31, 30, 31, 30,
+             31, 31, 30, 31, 30, 31 ],
+  '2'   => [ 59, 61, 61, 62, 61, 61 ],
+  '3'   => [ 90, 91, 92, 92 ],
+  '4'   => [ 120, 123, 122 ],
+  '6'   => [ 181, 184 ],
+  '12'  => [ 365 ],
+);
+
+sub check {
+  # yes, this package plan is such a special snowflake it needs its own
+  # check method.
+  my $self = shift;
+
+  if ( !exists($freq_max_days{$self->freq}) ) {
+    return 'Prorate (calendar cycle) billing interval must be an integer factor of one year';
+  }
+  $self->SUPER::check;
+}
+
+sub cutoff_day {
+  my( $self, $cust_pkg ) = @_;
+  my @periods = @{ $freq_cutoff_days{$self->freq} };
+  my @cutoffs = ($self->option('cutoff_day') || 1); # Jan 1 = 1
+  pop @periods; # we don't care about the last one
+  foreach (@periods) {
+    push @cutoffs, $cutoffs[-1] + $_;
+  }
+  @cutoffs;
+}
+
+sub calc_prorate {
+  # it's not the same algorithm
+  my ($self, $cust_pkg, $sdate, $details, $param, @cutoff_days) = @_;
+  die "no cutoff_day" unless @cutoff_days;
+  die "prepaid terms not supported with calendar prorate packages"
+    if $param->{freq_override}; # XXX if we ever use this again
+
+  #XXX should we still be doing this with multi-currency support?
+  my $money_char = FS::Conf->new->config('money_char') || '$';
+
+  my $charge = $self->base_recur($cust_pkg, $sdate) || 0;
+  my $now = DateTime->from_epoch(epoch => $$sdate, time_zone => 'local');
+
+  my $add_period = 0;
+  # if this is the first bill but the bill date has been set
+  # (by prorate_defer_bill), calculate from the setup date,
+  # append the setup fee to @$details, and make sure to bill for 
+  # a full period after the bill date.
+
+  if ( $self->option('prorate_defer_bill', 1)
+    and !$cust_pkg->getfield('last_bill')
+    and $cust_pkg->setup )
+  {
+    $param->{'setup_fee'} = $self->calc_setup($cust_pkg, $$sdate, $details);
+    $now = DateTime->from_epoch(epoch => $cust_pkg->setup, time_zone => 'local');
+    $add_period = 1;
+  }
+
+  # DON'T sync to the existing billing day; cutoff days work differently here.
+
+  $now->truncate(to => 'day');
+  my ($end, $start) = $self->calendar_endpoints($now, @cutoff_days);
+
+  #warn "[prorate_calendar] now = ".$now->ymd.", start = ".$start->ymd.", end = ".$end->ymd."\n";
+
+  my $periods = $end->delta_days($now)->delta_days /
+                $end->delta_days($start)->delta_days;
+  if ( $periods < 1 and $add_period ) {
+    $periods++; # charge for the extra time
+    $start->add(months => $self->freq); # and push the next bill date forward
+  }
+  if ( $self->option('prorate_verbose',1) and $periods > 0 ) {
+    if ( $periods < 1 ) {
+      push @$details,
+        'Prorated (' . $now->strftime('%b %d') .
+        ' - ' . $end->strftime('%b %d') . '): ' . $money_char .
+        sprintf('%.2f', $charge * $periods + 0.00000001);
+    } elsif ( $periods > 1 ) {
+      push @$details,
+        'Prorated (' . $now->strftime('%b %d') .
+        ' - ' . $end->strftime('%b %d') . '): ' . $money_char .
+        sprintf('%.2f', $charge * ($periods - 1) + 0.00000001),
+
+        'First full period: ' . $money_char . sprintf('%.2f', $charge);
+    } # else exactly one period
+  }
+
+  $$sdate = $start->epoch;
+  return sprintf('%.2f', $charge * $periods + 0.00000001);
+}
+
+sub prorate_setup {
+  my $self = shift;
+  my ($cust_pkg, $sdate) = @_;
+  my @cutoff_days = $self->cutoff_day;
+  if ( ! $cust_pkg->bill
+     and $self->option('prorate_defer_bill')
+     and @cutoff_days )
+  {
+    my $now = DateTime->from_epoch(epoch => $sdate, time_zone => 'local');
+    $now->truncate(to => 'day');
+    my ($end, $start) = $self->calendar_endpoints($now, @cutoff_days);
+    if ( $now->compare($start) == 0 ) {
+      $cust_pkg->setup($start->epoch);
+      $cust_pkg->bill($start->epoch);
+    } else {
+      $cust_pkg->bill($end->epoch);
+    }
+    return 1;
+  } else {
+    return 0;
+  }
+}
+
+=item calendar_endpoints NOW CUTOFF_DAYS
+
+Given a current date (DateTime object) and a list of cutoff day-of-year
+numbers, finds the next upcoming cutoff day (in either the current or the 
+upcoming year) and the cutoff day before that, and returns them both.
+
+=cut
+
+sub calendar_endpoints {
+  my $self = shift;
+  my $now = shift;
+  my @cutoff_day = sort {$a <=> $b} @_;
+
+  my $year = $now->year;
+  my $day = $now->day_of_year;
+  # Feb 29 = 60 
+  # For cutoff day purposes, it's the same day as Feb 28
+  $day-- if $now->is_leap_year and $day >= 60;
+
+  # select the first cutoff day that's after the current day
+  my $i = 0;
+  while ( $cutoff_day[$i] and $cutoff_day[$i] <= $day ) {
+    $i++;
+  }
+  # $cutoff_day[$i] is now later in the calendar than today
+  # or today is between the last cutoff day and the end of the year
+
+  my ($start, $end);
+  if ( $i == 0 ) {
+    # then today is on or before the first cutoff day
+    $start = DateTime->from_day_of_year(year => $year - 1,
+                                        day_of_year => $cutoff_day[-1],
+                                        time_zone => 'local');
+    $end =   DateTime->from_day_of_year(year => $year,
+                                        day_of_year => $cutoff_day[0],
+                                        time_zone => 'local');
+  } elsif ( $i > 0 and $i < scalar(@cutoff_day) ) {
+    # today is between two cutoff days
+    $start = DateTime->from_day_of_year(year => $year,
+                                        day_of_year => $cutoff_day[$i - 1],
+                                        time_zone => 'local');
+    $end =   DateTime->from_day_of_year(year => $year,
+                                        day_of_year => $cutoff_day[$i],
+                                        time_zone => 'local');
+  } else {
+    # today is after the last cutoff day
+    $start = DateTime->from_day_of_year(year => $year,
+                                        day_of_year => $cutoff_day[-1],
+                                        time_zone => 'local');
+    $end =   DateTime->from_day_of_year(year => $year + 1,
+                                        day_of_year => $cutoff_day[0],
+                                        time_zone => 'local');
+  }
+  return ($end, $start);
+}
+
+1;
index ef260cf..6d58a3d 100644 (file)
@@ -226,7 +226,7 @@ sub payinfo_check {
 
 }
 
-=item payby_payinfo_pretty
+=item payby_payinfo_pretty [ LOCALE ]
 
 Returns payment method and information (suitably masked, if applicable) as
 a human-readable string, such as:
@@ -241,22 +241,25 @@ or
 
 sub payby_payinfo_pretty {
   my $self = shift;
+  my $locale = shift;
+  my $lh = FS::L10N->get_handle($locale);
   if ( $self->payby eq 'CARD' ) {
-    'Card #'. $self->paymask;
+    $lh->maketext('Card #') . $self->paymask;
   } elsif ( $self->payby eq 'CHEK' ) {
-    'E-check acct#'. $self->payinfo;
+    $lh->maketext('E-check acct#') . $self->payinfo;
   } elsif ( $self->payby eq 'BILL' ) {
-    'Check #'. $self->payinfo;
+    $lh->maketext('Check #') . $self->payinfo;
   } elsif ( $self->payby eq 'PREP' ) {
-    'Prepaid card #'. $self->payinfo;
+    $lh->maketext('Prepaid card #') . $self->payinfo;
   } elsif ( $self->payby eq 'CASH' ) {
-    'Cash '. $self->payinfo;
+    $lh->maketext('Cash') . ' ' . $self->payinfo;
   } elsif ( $self->payby eq 'WEST' ) {
-    'Western Union'; #. $self->payinfo;
+    # does Western Union localize their name?
+    $lh->maketext('Western Union');
   } elsif ( $self->payby eq 'MCRD' ) {
-    'Manual credit card'; #. $self->payinfo;
+    $lh->maketext('Manual credit card');
   } elsif ( $self->payby eq 'PPAL' ) {
-    'PayPal transaction#' . $self->order_number;
+    $lh->maketext('PayPal transaction#') . $self->order_number;
   } else {
     $self->payby. ' '. $self->payinfo;
   }
index 82c875a..bdeaf1b 100644 (file)
@@ -131,35 +131,78 @@ sub sales_cust_main {
   qsearchs( 'cust_main', { 'custnum' => $self->sales_custnum } );
 }
 
-sub cust_bill_pkg {
+=item cust_bill_pkg START END OPTIONS
+
+Returns the package line items (see L<FS::cust_bill_pkg>) for which this 
+sales person could receive commission.
+
+START and END are an optional date range to limit the results.
+
+OPTIONS may contain:
+- I<cust_main_sales>: if this is a true value, sales of packages that have no
+package sales person will be included if this is their customer sales person.
+- I<classnum>: limit to this package classnum.
+- I<paid>: limit to sales that have no unpaid balance.
+
+=cut
+
+sub cust_bill_pkg_search {
   my( $self, $sdate, $edate, %search ) = @_;
 
   my $cmp_salesnum = delete $search{'cust_main_sales'}
                        ? ' COALESCE( cust_pkg.salesnum, cust_main.salesnum )'
                        : ' cust_pkg.salesnum ';
 
+  my $salesnum = $self->salesnum;
+  die "bad salesnum" unless $salesnum =~ /^(\d+)$/;
+  my @where = ( "$cmp_salesnum    = $salesnum",
+                "sales_pkg_class.salesnum = $salesnum"
+              );
+  push @where, "cust_bill._date >= $sdate" if $sdate;
+  push @where, "cust_bill._date  < $edate" if $edate;
+
   my $classnum_sql = '';
   if ( exists( $search{'classnum'}  ) ) {
-    my $classnum = $search{'classnum'};
-    $classnum_sql = " AND part_pkg.classnum ". ( $classnum ? " = $classnum "
-                                                           : ' IS NULL '     );
+    my $classnum = $search{'classnum'} || '';
+    die "bad classnum" unless $classnum =~ /^(\d*)$/;
+
+    push @where,
+      "part_pkg.classnum ". ( $classnum ? " = $classnum " : ' IS NULL ' );
   }
 
-  qsearch({ 'table'     => 'cust_bill_pkg',
-            'select'    => 'cust_bill_pkg.*',
-            'addl_from' => ' LEFT JOIN cust_bill USING ( invnum ) '.
-                           ' LEFT JOIN cust_pkg  USING ( pkgnum ) '.
-                           ' LEFT JOIN part_pkg  USING ( pkgpart ) '.
-                           ' LEFT JOIN cust_main ON ( cust_pkg.custnum = cust_main.custnum )',
-            'extra_sql' => ( keys %{ $search{'hashref'} }
-                               ? ' AND ' : 'WHERE '
-                           ).
-                           "     cust_bill._date >= $sdate ".
-                           " AND cust_bill._date  < $edate ".
-                           " AND $cmp_salesnum = ". $self->salesnum.
-                           $classnum_sql,
-            #%search,
-         });
+  # sales_pkg_class number-of-months limit, grr
+  # (we should be able to just check for the cust_event record from the 
+  # commission credit, but the report is supposed to act as a check on that)
+  #
+  # Pg-specific, of course
+  my $setup_date = 'TO_TIMESTAMP( cust_pkg.setup )';
+  my $interval = "(sales_pkg_class.commission_duration || ' months')::interval";
+  my $charge_date = 'TO_TIMESTAMP( cust_bill._date )';
+  push @where, "CASE WHEN sales_pkg_class.commission_duration IS NOT NULL ".
+               "THEN $charge_date < $setup_date + $interval ".
+               "ELSE TRUE END";
+
+  if ( $search{'paid'} ) {
+    push @where, FS::cust_bill_pkg->owed_sql . ' <= 0.005';
+  }
+
+  my $extra_sql = "WHERE ".join(' AND ', map {"( $_ )"} @where);
+
+  { 'table'     => 'cust_bill_pkg',
+    'select'    => 'cust_bill_pkg.*',
+    'addl_from' => ' LEFT JOIN cust_bill USING ( invnum ) '.
+                   ' LEFT JOIN cust_pkg  USING ( pkgnum ) '.
+                   ' LEFT JOIN part_pkg  USING ( pkgpart ) '.
+                   ' LEFT JOIN cust_main ON ( cust_pkg.custnum = cust_main.custnum )'.
+                   ' JOIN sales_pkg_class ON ( '.
+                   ' COALESCE( sales_pkg_class.classnum, 0) = COALESCE( part_pkg.classnum, 0) )',
+    'extra_sql' => $extra_sql,
+ };
+}
+
+sub cust_bill_pkg {
+  my $self = shift;
+  qsearch( $self->cust_bill_pkg_search(@_) )
 }
 
 sub cust_credit {
index 052836e..5497c72 100644 (file)
@@ -90,6 +90,13 @@ sub check {
     $self->ut_numbern('towernum')
     || $self->ut_text('towername')
     || $self->ut_enum('disabled', [ '', 'Y' ])
+    || $self->ut_coordn('latitude')
+    || $self->ut_coordn('longitude')
+    || $self->ut_enum('coord_auto', [ '', 'Y' ])
+    || $self->ut_floatn('altitude')
+    || $self->ut_floatn('height')
+    || $self->ut_floatn('veg_height')
+    || $self->ut_alphan('color')
   ;
   return $error if $error;
 
index 3605190..70642fb 100644 (file)
@@ -108,6 +108,11 @@ sub check {
     || $self->ut_number('towernum', 'tower', 'towernum')
     || $self->ut_text('sectorname')
     || $self->ut_textn('ip_addr')
+    || $self->ut_floatn('height')
+    || $self->ut_numbern('freq_mhz')
+    || $self->ut_numbern('direction')
+    || $self->ut_numbern('width')
+    || $self->ut_floatn('range')
   ;
   return $error if $error;
 
index 7a460da..e635831 100644 (file)
@@ -1,6 +1,5 @@
 Changes
 MANIFEST
-MANIFEST.SKIP
 Makefile.PL
 bin/freeside-addoutsource
 bin/freeside-addoutsourceuser
@@ -728,3 +727,5 @@ FS/cable_provider.pm
 t/cable_provider.t
 FS/cust_credit_void.pm
 t/cust_credit_void.t
+FS/discount_class.pm
+t/discount_class.t
index 06ec962..7cacf10 100755 (executable)
@@ -1,8 +1,8 @@
 #!/usr/bin/perl -w
 
 use strict;
-use vars qw($opt_d $opt_s $opt_q $opt_v $opt_r);
-use vars qw($DEBUG $DRY_RUN);
+use vars qw( $opt_d $opt_s $opt_q $opt_v $opt_r $opt_c );
+use vars qw( $DEBUG $DRY_RUN );
 use Getopt::Std;
 use DBIx::DBSchema 0.31; #0.39
 use FS::UID qw(adminsuidsetup checkeuid datasrc driver_name);
@@ -17,7 +17,7 @@ my $start = time;
 
 die "Not running uid freeside!" unless checkeuid();
 
-getopts("dqrs");
+getopts("dqrcs");
 
 $DEBUG = !$opt_q;
 #$DEBUG = $opt_v;
@@ -154,6 +154,18 @@ unless ( driver_name =~ /^mysql/i ) {
          @statements;
 }
 
+if ( $opt_c ) {
+
+  @statements =
+    grep { $_ !~ /^ *ALTER +TABLE +(h_)?cdr /i }
+         @statements;
+
+  @statements =
+    grep { $_ !~ /^ *CREATE +INDEX +(h_)?cdr\d+ /i }
+         @statements;
+
+}
+
 if ( $DRY_RUN ) {
   print
     join(";\n", @statements ). ";\n";
@@ -312,7 +324,7 @@ freeside-upgrade - Upgrades database schema for new freeside verisons.
 
 =head1 SYNOPSIS
 
-  freeside-upgrade [ -d ] [ -r ] [ -s ] [ -q | -v ]
+  freeside-upgrade [ -d ] [ -r ] [ -c ] [ -s ] [ -q | -v ]
 
 =head1 DESCRIPTION
 
@@ -337,6 +349,8 @@ Also performs other upgrade functions:
   [ -r ]: Skip sqlradius updates.  Useful for occassions where the sqlradius
           databases may be inaccessible.
 
+  [ -c ]: Skip cdr and h_cdr updates.
+
   [ -v ]: Run verbosely, sending debugging information to STDERR.  This is the
           current default.
 
diff --git a/FS/t/discount_class.t b/FS/t/discount_class.t
new file mode 100644 (file)
index 0000000..1ccf92e
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::discount_class;
+$loaded=1;
+print "ok 1\n";
index 9c71b3a..0a6f851 100644 (file)
@@ -108,7 +108,7 @@ The author forgot to customize this manpage.
 
 =head1 SEE ALSO
 
-L<FS::Record>, schema.html from the base documentation.
+L<FS::Record>
 
 =cut
 
index f26c161..d3cf873 100644 (file)
@@ -8,8 +8,9 @@
                  'count_query' => 'SELECT COUNT(*) FROM discount',
                  'disableable' => 1,
                  'disabled_statuspos' => 1,
-                 'header'      => [ 'Name', 'Discount', ],
+                 'header'      => [ 'Name', 'Class', 'Discount', ],
                  'fields'      => [ 'name',
+                                    'classname',
                                     'description',
                                   ],
                  'links'       => [ $link,
diff --git a/httemplate/browse/discount_class.html b/httemplate/browse/discount_class.html
new file mode 100644 (file)
index 0000000..7f09102
--- /dev/null
@@ -0,0 +1,34 @@
+<% include( 'elements/browse.html',
+                 'title'       => 'Discount classes',
+                 'html_init'   => $html_init,
+                 'name'        => 'discount classes',
+                 'disableable' => 1,
+                 'disabled_statuspos' => 1,
+                 'query'       => { 'table'     => 'discount_class',
+                                    'hashref'   => {},
+                                    'order_by' => 'ORDER BY classnum',
+                                  },
+                 'count_query' => $count_query,
+                 'header'      => $header,
+                 'fields'      => $fields,
+                 'links'       => $links,
+             )
+%>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $html_init = 
+  'Discount classes define reporing classifications for discounts.<BR><BR>'.
+  qq!<A HREF="${p}edit/discount_class.html"><I>Add a discount class</I></A><BR><BR>!;
+
+my $count_query = 'SELECT COUNT(*) FROM discount_class';
+
+my $link = [ $p.'edit/discount_class.html?', 'classnum' ];
+
+my $header = [ '#', 'Class' ];
+my $fields = [ 'classnum', 'classname' ];
+my $links  = [ $link, $link ];
+
+</%init>
index 7f096a7..e2f9fd0 100644 (file)
@@ -8,12 +8,13 @@
                  'count_query' => 'SELECT COUNT(*) FROM tower',
                  'disableable' => 1,
                  'disabled_statuspos' => 1,
-                 'header'      => [ 'Name', 'Sectors', 'Coordinates'],
+                 'header'      => [ 'Name', 'Location', 'Sectors', ],
                  'fields'      => [ $tower_sub,
-                                    $sector_sub,
                                     $coord_sub,
+                                    $sector_sub,
                                   ],
                  'links'       => [ ],
+                 'cell_style'    => [ $tagdesc_style ],
              )
 %>
 <%init>
@@ -21,6 +22,9 @@
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
 
+#false laziness w/ browse/part_tag.html
+my $tagdesc_style = sub { 'background-color:#'.shift->color };
+
 my $num_svc_links = sub {
   my ($query_string, $sectors) = @_;
   return if !$sectors;
@@ -57,7 +61,12 @@ my $coord_sub = sub {
 
   [
     [
-      { 'data' => "Latitude: " . $tower->latitude . "<br>Longitude: " . $tower->longitude, },
+      { 'data' => "Latitude: ".              $tower->latitude.
+                  "<br>Longitude: ".         $tower->longitude.
+                  "<br>Altitude: ".          $tower->altitude.
+                  "<br>Height: ".            $tower->height.
+                  "<br>Veg. height: ". $tower->veg_height,
+      },
       { 'data' => $coords, 'link' => "Coordinates", },
     ],
   ]
@@ -70,7 +79,7 @@ my $tower_sub = sub {
   );
   [ #rows
     [
-      { 'data' => $tower->towername, },
+      { 'data' => $tower->towername. '&nbsp;', },
       { 'data' => ' (edit) ', size => '-1', 
         'link' => $p.'edit/tower.html?' . $tower->towernum },
     ],
@@ -89,7 +98,7 @@ my $sector_sub = sub {
       my $sectornum = $sector->sectornum;
       [
         {
-          'data' => $sector->sectorname,
+          'data' => $sector->sectorname. '&nbsp;',
           'link' => ( $sector->ip_addr ? 'http://'. $sector->ip_addr : '' ),
         },
         
index b087943..2d39f96 100644 (file)
@@ -7,17 +7,19 @@
                                'last',
                                'first',
                                { field=>'user_custnum', type=>'search-cust_main', },
+                               { field=>'report_salesnum', type=>'select-sales', empty_label=>'all', },
                                { field=>'disabled', type=>'checkbox', value=>'Y' },
                              ],
                  'labels' => { 
-                               'usernum'      => 'User number',
-                               'username'     => 'Username',
-                               '_password'    => 'Password',
-                               '_password2'   => 'Re-enter Password',
-                               'last'         => 'Last name',
-                               'first'        => 'First name',
-                               'user_custnum' => 'Customer (optional)',
-                               'disabled'     => 'Disable employee',
+                               'usernum'         => 'User number',
+                               'username'        => 'Username',
+                               '_password'       => 'Password',
+                               '_password2'      => 'Re-enter Password',
+                               'last'            => 'Last name',
+                               'first'           => 'First name',
+                               'user_custnum'    => 'Customer (optional)',
+                               'report_salesnum' => 'Limit commission report to sales person',
+                               'disabled'        => 'Disable employee',
                              },
                  'edit_callback' => \&edit_callback,
                  'field_callback'=> \&field_callback,
@@ -68,8 +70,8 @@ my $check_user_custnum_search = <<END;
 END
 
 sub edit_callback {
-  my ($c, $o, $f, $opt) = @_;
-  $o->set('_password', '');
+  my ($cgi, $access_user, $fields_listref, $opt_hashref) = @_;
+  $access_user->_password('');
 }
 
 sub field_callback {
index 9bcd1e7..c2853bd 100644 (file)
@@ -3,6 +3,7 @@
                  'table'  => 'discount',
                  'fields' => [
                                'name',
+                               { field => 'classnum', type => 'select-discount_class' },
                                { field => 'disabled', type => 'checkbox', value=>'Y', },
                                # a weird kind of false laziness
                                # w/elements/tr-select-discount.html
@@ -27,6 +28,7 @@
                  'labels' => { 
                                'discountnum' => 'Discount #',
                                'name'        => 'Name&nbsp;',
+                               'classnum'    => 'Class',
                                'disabled'    => 'Disabled&nbsp;',
                                '_type'       => 'Type&nbsp;',
                                'amount'      => 'Amount&nbsp;',
diff --git a/httemplate/edit/discount_class.html b/httemplate/edit/discount_class.html
new file mode 100644 (file)
index 0000000..2bf27d9
--- /dev/null
@@ -0,0 +1,10 @@
+<% include( 'elements/class_Common.html',
+              'name_singular'   => 'Discount class',
+              'table'  => 'discount_class',
+             'nocat' => 1,
+              'addl_labels' => { 'classnum'  => 'Class',
+                                 'classname' => 'Class',
+                                 'disabled'  => 'Disable',
+                               },
+          )
+%>
index 0a0916e..723227f 100644 (file)
@@ -30,7 +30,13 @@ unless ( $opt{'nocat'} ) {
 
 my $fields = [   'classname',
         (scalar(@category)
-          ? { field=>'categorynum', type=>'select-table', 'empty_label'=>'(none)', 'table'=>$category_table, 'name_col'=>'categoryname' }
+          ? { field       => 'categorynum',
+              type        => 'select-table',
+              table       => $category_table,
+              hashref     => { 'disabled' => '' },
+              name_col    => 'categoryname',
+              empty_label => '(none)',
+            }
           : { field=>'categorynum', type=>'hidden' }
         ),
         { field=>'disabled', type=>'checkbox', value=>'Y', },
index 321c685..4131508 100644 (file)
 
                    my $columndef = $part_svc->part_svc_column($f->{'field'});
                    my $flag = $columndef->columnflag;
-                   if ( $flag eq 'F' ) {
+
+                   if ( $flag eq 'F' ) { #fixed
                      $f->{'type'} = length($columndef->columnvalue)
                                       ? 'fixed'
                                       : 'hidden';
                      $f->{'value'} = $columndef->columnvalue;
-                   } elsif ( $flag eq 'A' ) {
+
+                   } elsif ( $flag eq 'A' ) { #auto assign from inventory
                      $f->{'type'} = 'hidden';
-                   } elsif ( $flag eq 'M' ) {
+
+                   } elsif ( $flag eq 'M' ) { #manually assign from inventory
                      $f->{'type'} = 'select-inventory_item';
                      $f->{'empty_label'} = 'Select inventory item';
                      $f->{'extra_sql'} = 'WHERE ( svcnum IS NULL ' .
                         ')';
                      $f->{'classnum'} = $columndef->columnvalue;
                      $f->{'disable_empty'} = $object->svcnum ? 1 : 0;
-                   } elsif ( $flag eq 'H' ) {
+
+                   } elsif ( $flag eq 'H' ) { #hardware
                      $f->{'type'}        = 'select-hardware_type';
                      $f->{'hashref'}     = {
                                             'classnum'=>$columndef->columnvalue
                                            };
+
+                   } elsif ( $flag eq 'S' ) { #selectable choice
+                     $f->{type}    = 'select';
+                     $f->{options} = [ split( /\s*,\s*/,
+                                                $columndef->columnvalue)
+                                     ];
                    }
 
                    if (    $f->{'type'} eq 'select-svc_pbx'
diff --git a/httemplate/edit/process/discount_class.html b/httemplate/edit/process/discount_class.html
new file mode 100644 (file)
index 0000000..e724946
--- /dev/null
@@ -0,0 +1,11 @@
+<% include( 'elements/process.html',
+               'table'       => 'discount_class',
+               'viewall_dir' => 'browse',
+           )
+%>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+</%init>
index 7353784..bbfc1a6 100644 (file)
@@ -2,6 +2,6 @@
     table       => 'tower',
     viewall_dir => 'browse',
     process_o2m => { 'table'  => 'tower_sector',
-                     'fields' => [qw( sectorname ip_addr )],
+                     'fields' => [qw( sectorname ip_addr height freq_mhz direction width range )],
                    },
 &>
index 673a271..00c9add 100644 (file)
@@ -4,24 +4,32 @@
      viewall_dir   => 'browse',
      fields        => [ 'towername',
                         { field=>'disabled', type=>'checkbox', value=>'Y', },
-                        { field             => 'default_ip_addr',
+                        { field=>'color',    type=>'pickcolor' },
+                        { field               => 'default_ip_addr',
                           curr_value_callback => $default_ip_addr_callback },
+                        'latitude',
+                        'longitude',
+                        'altitude',
+                        'height',
+                        'veg_height',
                         { field             => 'sectornum',
                           type              => 'tower_sector',
                           o2m_table         => 'tower_sector',
                           m2_label          => 'Sector',
                           m2_error_callback => $m2_error_callback,
                         },
-                        'latitude',
-                        'longitude',
                       ],
-     labels        => { 'towernum'  => 'Tower',
-                        'towername' => 'Name',
-                        'sectornum' => 'Sector',
-                        'disabled'  => 'Disabled',
+     labels        => { 'towernum'        => 'Tower',
+                        'towername'       => 'Name',
+                        'sectornum'       => 'Sector',
+                        'disabled'        => 'Disabled',
                         'default_ip_addr' => 'Tower IP address',
-                        'latitude' => 'Latitude',
-                        'longitude' => 'Longitude',
+                        'latitude'        => 'Latitude',
+                        'longitude'       => 'Longitude',
+                        'altitude'        => 'Altitude',
+                        'height'          => 'Height',
+                        'veg_height'      => 'Vegetation height',
+                        'color'           => 'Color',
                       },
 &>
 <%init>
@@ -29,7 +37,7 @@
 my $m2_error_callback = sub { # reconstruct the list
   my ($cgi, $object) = @_;
 
-  my @fields = qw(sectorname ip_addr);
+  my @fields = qw( sectorname ip_addr height freq_mhz direction width range );
   map {
     my $k = $_;
     new FS::tower_sector {
index 3d51776..8abce05 100644 (file)
@@ -31,7 +31,7 @@
 %           'phonetypenum' => $1,
 %         });
 %         if ( $contact_phone ) {
-%           $value = $contact_phone->phonenum;
+%           $value = $contact_phone->phonenum_pretty;
 %           $value .= 'x'.$contact_phone->extension
 %             if $contact_phone->extension;
 %           $value = '+'. $contact_phone->countrycode. " $value"
index 8279415..18272eb 100644 (file)
@@ -5,6 +5,7 @@
                            <% $size %>
                            <% $maxlength %>
                            <% $style %>
+                           <% $opt{autocomplete} ? 'autocomplete="off"' : '' %>
                            <% $opt{disabled} %>
                            <% $onchange %>
                     ><% $opt{'postfix'} %>
index 8cb9675..b857523 100644 (file)
@@ -564,6 +564,7 @@ if ( $curuser->access_right('Configuration') ) {
   #eo package grouping sub-menu
 
   $config_pkg{'Discounts'} = [ $fsurl.'browse/discount.html', '' ];
+  $config_pkg{'Discount classes'} = [ $fsurl.'browse/discount_class.html', '' ];
   $config_pkg{'Cancel/Suspend Reasons'} = [ \%config_pkg_reason, '' ];
 }
 
diff --git a/httemplate/elements/select-discount_class.html b/httemplate/elements/select-discount_class.html
new file mode 100644 (file)
index 0000000..41a27c5
--- /dev/null
@@ -0,0 +1,18 @@
+<% include( '/elements/select-table.html',
+                 'table'       => 'discount_class',
+                 'name_col'    => 'classname',
+                 'value'       => $classnum,
+                 'empty_label' => '(none)',
+                 'hashref'     => { 'disabled' => '' },
+                 %opt,
+             )
+%>
+<%init>
+
+my %opt = @_;
+my $classnum = $opt{'curr_value'} || $opt{'value'};
+
+$opt{'records'} = delete $opt{'discount_class'}
+  if $opt{'discount_class'};
+
+</%init>
index ad774d8..debd9e7 100644 (file)
@@ -202,23 +202,32 @@ function post_standardization() {
 
   var cf = document.<% $formname %>;
 
-  if ( new String(cf.elements['<% $taxpre %>zip'].value).length < 10 )
+  var prefix = '<% $taxpre %>';
+  // fix edge case with cust_main
+  if ( cf.elements['same']
+    && cf.elements['same'].checked
+    && prefix == 'ship_' ) {
+
+    prefix = 'bill_';
+  }
+
+  if ( new String(cf.elements[prefix + 'zip'].value).length < 10 )
   {
 
-    var country_el = cf.elements['<% $taxpre %>country'];
+    var country_el = cf.elements[prefix + 'country'];
     var country = country_el.options[ country_el.selectedIndex ].value;
-    var geocode = cf.elements['<% $taxpre %>geocode'].value;
+    var geocode = cf.elements[prefix + 'geocode'].value;
 
     if ( country == 'CA' || country == 'US' ) {
 
-      var state_el = cf.elements['<% $taxpre %>state'];
+      var state_el = cf.elements[prefix + 'state'];
       var state = state_el.options[ state_el.selectedIndex ].value;
 
       var url = "<% $p %>/misc/choose_tax_location.html" +
                   "?data_vendor=cch-zip" + 
-                  ";city="     + cf.elements['<% $taxpre %>city'].value +
+                  ";city="     + cf.elements[prefix + 'city'].value +
                   ";state="    + state + 
-                  ";zip="      + cf.elements['<% $taxpre %>zip'].value +
+                  ";zip="      + cf.elements[prefix + 'zip'].value +
                   ";country="  + country +
                   ";geocode="  + geocode +
                   ";formname=" + '<% $formname %>' +
@@ -229,14 +238,14 @@ function post_standardization() {
 
     } else {
 
-      cf.elements['<% $taxpre %>geocode'].value = 'DEFAULT';
+      cf.elements[prefix + 'geocode'].value = 'DEFAULT';
       <% $post_geocode %>;
 
     }
 
   } else {
 
-    cf.elements['<% $taxpre %>geocode'].value = '';
+    cf.elements[prefix + 'geocode'].value = '';
     <% $post_geocode %>;
 
   }
@@ -255,13 +264,19 @@ function update_geocode() {
   set_geocode = function (what) {
 
     var cf = document.<% $formname %>;
+    var prefix = '<% $taxpre %>';
+    if ( cf.elements['same']
+      && cf.elements['same'].checked
+      && prefix == 'ship_' ) {
+      prefix = 'bill_';
+    }
 
     //alert(what.options[what.selectedIndex].value);
     var argsHash = eval('(' + what.options[what.selectedIndex].value + ')');
-    cf.elements['<% $taxpre %>city'].value     = argsHash['city'];
-    setselect(cf.elements['<% $taxpre %>state'], argsHash['state']);
-    cf.elements['<% $taxpre %>zip'].value      = argsHash['zip'];
-    cf.elements['<% $taxpre %>geocode'].value  = argsHash['geocode'];
+    cf.elements[prefix + 'city'].value     = argsHash['city'];
+    setselect(cf.elements[prefix + 'state'], argsHash['state']);
+    cf.elements[prefix + 'zip'].value      = argsHash['zip'];
+    cf.elements[prefix + 'geocode'].value  = argsHash['geocode'];
     <% $post_geocode %>;
 
   }
index a8bbbc5..406895e 100644 (file)
@@ -53,6 +53,11 @@ my %size = ( 'title' => 12 );
 tie my %label, 'Tie::IxHash',
   'sectorname' => 'Name',
   'ip_addr'    => 'IP Address',
+  'height'     => 'Height',
+  'freq_mhz'   => 'Freq. (MHz)',
+  'direction'  => 'Direction', # or a button to set these to 0 for omni
+  'width'      => 'Width',     #
+  'range'      => 'Range',
 ;
 
 my @fields = keys %label;
index ffc9038..be6e03c 100644 (file)
@@ -7,7 +7,7 @@
 
 <TR>
   <TD ALIGN="right">From date: </TD>
-  <TD><INPUT TYPE="text" NAME="<% $opt{prefix} %>beginning" ID="<% $opt{prefix} %>beginning_text" VALUE="<% $from %>" SIZE=<%$size%> MAXLENGTH=<%$maxlength%>> <IMG SRC="<%$fsurl%>images/calendar.png" ID="<% $opt{prefix} %>beginning_button" STYLE="cursor: pointer" TITLE="Select date"><IMG SRC="<%$fsurl%>images/calendar-disabled.png" ID="<% $opt{prefix} %>beginning_disabled" STYLE="display:none"><BR><i>m/d/y<% $time_hint %></i></TD>
+  <TD><INPUT TYPE="text" NAME="<% $opt{prefix} %>beginning" ID="<% $opt{prefix} %>beginning_text" VALUE="<% $from ? time2str($date_format, $from) : '' %>" SIZE=<%$size%> MAXLENGTH=<%$maxlength%>> <IMG SRC="<%$fsurl%>images/calendar.png" ID="<% $opt{prefix} %>beginning_button" STYLE="cursor: pointer" TITLE="Select date"><IMG SRC="<%$fsurl%>images/calendar-disabled.png" ID="<% $opt{prefix} %>beginning_disabled" STYLE="display:none"><BR><i>m/d/y<% $time_hint %></i></TD>
 <SCRIPT TYPE="text/javascript">
   Calendar.setup({
     inputField: "<% $opt{prefix} %>beginning_text",
@@ -26,7 +26,7 @@
 % }
 
   <TD ALIGN="right">To date: </TD>
-  <TD><INPUT TYPE="text" NAME="<% $opt{prefix} %>ending" ID="<% $opt{prefix} %>ending_text" VALUE="<% $to %>" SIZE=<%$size%> MAXLENGTH=<%$maxlength%>> <IMG SRC="<%$fsurl%>images/calendar.png" ID="<% $opt{prefix} %>ending_button" STYLE="cursor: pointer" TITLE="Select date"><IMG SRC="<%$fsurl%>images/calendar-disabled.png" ID="<% $opt{prefix} %>ending_disabled" STYLE="display:none"><BR><i>m/d/y<% $time_hint %></i></TD>
+  <TD><INPUT TYPE="text" NAME="<% $opt{prefix} %>ending" ID="<% $opt{prefix} %>ending_text" VALUE="<% $to ? time2str($date_format, $to) : '' %>" SIZE=<%$size%> MAXLENGTH=<%$maxlength%>> <IMG SRC="<%$fsurl%>images/calendar.png" ID="<% $opt{prefix} %>ending_button" STYLE="cursor: pointer" TITLE="Select date"><IMG SRC="<%$fsurl%>images/calendar-disabled.png" ID="<% $opt{prefix} %>ending_disabled" STYLE="display:none"><BR><i>m/d/y<% $time_hint %></i></TD>
 <SCRIPT TYPE="text/javascript">
   Calendar.setup({
     inputField: "<% $opt{prefix} %>ending_text",
index 33725b9..922f22f 100644 (file)
@@ -5,6 +5,7 @@
 % }
 <& /elements/tr-input-text.html, id => $id, @_ &>
 <script type="text/javascript">
+<&| /elements/onload.js &>
 MaskedInput({
   elm: document.getElementById('<%$id%>'),
   format: '<% $opt{format} %>',
@@ -12,7 +13,41 @@ MaskedInput({
   <% $opt{typeon}  ? "typeon:  '$opt{typeon}',"  : '' %>
 });
 document.getElementById('<%$id%>').value = <% $value |js_string %>;
+% if ( $clipboard_hack ) {
+var t = document.getElementById('<% $id %>');
+var container = document.getElementById('<%$id%>_clipboard');
+var KeyHandlerDown = t.onkeydown
+t.onkeydown = function(e) {
+  // intercept ctrl-c and ctrl-x
+  // and cmd-c and cmd-x on mac
+  // when text is selected
+  if ( ( e.ctrlKey || e.metaKey ) ) {
+    // do the dance
+    var separators = /[\\/:-]/g;
+    var s = t.value.substr(t.selectionStart, t.selectionEnd);
+    if ( s ) {
+      container.value = s.replace(separators, '');
+      container.previous = t;
+      container.focus();
+      container.select();
+      return true;
+    }
+  }
+  return KeyHandlerDown.call(t, e);
+};
+container.onkeyup = function(e) {
+  if ( container.previous ) {
+    setTimeout(function() {
+      container.previous.value = container.value;
+      container.previous.focus();
+    }, 10);
+  }
+  return true;
+}
+% } # clipboard hack
+</&>
 </script>
+<textarea id="<%$id%>_clipboard" style="opacity:0"></textarea>
 <%shared>
 my $init = 0;
 </%shared>
@@ -21,6 +56,8 @@ my %opt = @_;
 # must have a DOM id
 my $id = $opt{id} || sprintf('input%04d',int(rand(10000)));
 my $value = length($opt{curr_value}) ? $opt{curr_value} : $opt{value} || '';
+
+my $clipboard_hack = $FS::CurrentUser::CurrentUser->option('enable_mask_clipboard_hack');
 </%init>
 <%doc>
 Set up a text input field with input masking.
index d19c4ed..8ad303c 100644 (file)
@@ -1,3 +1,4 @@
 <& tr-input-text.html, @_,
-             'type'   => 'password',
+     'type'         => 'password',
+     'autocomplete' => 1,
 &>
diff --git a/httemplate/elements/tr-select-discount_class.html b/httemplate/elements/tr-select-discount_class.html
new file mode 100644 (file)
index 0000000..5489fe6
--- /dev/null
@@ -0,0 +1,27 @@
+% if ( scalar(@{ $opt{'discount_class'} }) == 0 ) { 
+
+  <INPUT TYPE="hidden" NAME="<% $opt{'element_name'} || $opt{'field'} || 'classnum' %>" VALUE="">
+
+% } else { 
+
+  <TR>
+    <TD ALIGN="right"><% $opt{'label'} || 'Discount class' %></TD>
+    <TD>
+      <% include( '/elements/select-discount_class.html',
+                    'curr_value' => $classnum,
+                    %opt
+                )
+      %>
+    </TD>
+  </TR>
+
+% } 
+
+<%init>
+
+my %opt = @_;
+my $classnum = $opt{'curr_value'} || $opt{'value'};
+
+$opt{'discount_class'} ||= [ qsearch( 'discount_class', { disabled=>'' } ) ];
+
+</%init>
index c599e71..6de84f8 100644 (file)
@@ -1,28 +1,35 @@
-<% include('/elements/header.html', 'Discount Report' ) %>
+<& /elements/header.html', 'Discount Report' &>
 
 <FORM ACTION="cust_bill_pkg_discount.html" METHOD="GET">
 
 <TABLE>
 
-<% include('/elements/tr-select-from_to.html' ) %>
+<!--
+  <& /elements/tr-select-discount_class.html,
+       'field'       => 'discount_classnum',
+       'pre_options' => [ '0' => 'all' ],
+       'empty_label' => '(none)'
+  &>
+-->
 
-<% include('/elements/tr-select-agent.html',
-             'label'         => 'For agent: ',
-             'disable_empty' => 0,
-          )
-%>
+  <& /elements/tr-select-from_to.html &>
 
-%# anything about line items, discounts or packages really
-%# otaker?
-%# package class?
-%# discount picker?  (discount classes and categories?  eek!)
+  <& /elements/tr-select-agent.html,
+       'label'         => 'For agent: ',
+       'disable_empty' => 0,
+  &>
+
+% # anything about line items, discounts or packages really
+% # otaker?
+% # package class?
+% # discount picker?  (discount classes and categories?  haha yup!)
 
 </TABLE>
 
 <BR><INPUT TYPE="submit" VALUE="Display">
 </FORM>
 
-<% include('/elements/footer.html') %>
+<& /elements/footer.html &>
 <%init>
 
 die "access denied"
index a436d08..e80f586 100644 (file)
@@ -4,12 +4,11 @@
 
 <TABLE>
 
-<% include( '/elements/tr-input-beginning_ending.html',
+<& /elements/tr-input-beginning_ending.html,
                 'datesrequired' => 1,
-                'from' => time2str('%m/%d/%Y',$from),
-                'to' => time2str('%m/%d/%Y',time),
-            ) 
-%>
+                'from' => $from,
+                'to' => time,
+&>
 
 <% include('/elements/tr-select-agent.html',
              'label'         => 'For agent: ',
index d06d0a8..3c6e2ae 100644 (file)
@@ -14,7 +14,7 @@
              
 %#  <FORM METHOD="POST" ACTION="<%$url_string%>loginout/login">
   <FORM METHOD="POST" ACTION="/login">
-    <INPUT TYPE="hidden" NAME="destination" VALUE="<% $r->prev->uri %>">
+    <INPUT TYPE="hidden" NAME="destination" VALUE="<% $r->prev->unparsed_uri %>">
 
     <TABLE CELLSPACING=0 CELLPADDING=4 BGCOLOR="#cccccc">
       <TR>
index 33d2219..2eae011 100644 (file)
@@ -19,52 +19,57 @@ Confirm address standardization
 % }
 % for my $pre (@prefixes) {
 %   my $name = $pre eq 'bill_' ? 'billing' : 'service';
-%   if ( $new{$pre.'addr_clean'} ) {
+%   if ( $new{$pre.'error'} ) {
   <TR>
     <TH>Entered <%$name%> address</TH>
-    <TH>Standardized <%$name%> address</TH>
   </TR>
-  <TR>
 %     if ( $old{$pre.'company'} ) {
   <TR>
     <TD><% $old{$pre.'company'} %></TD>
-    <TD><% $new{$pre.'company'} %></TD>
   </TR>
 %     }
   <TR>
     <TD><% $old{$pre.'address1'} %></TD>
-    <TD><% $new{$pre.'address1'} %></TD>
+    <TD ROWSPAN=3><FONT COLOR="#ff0000"><B><% $new{$pre.'error'} %></B></FONT></TD>
   </TR>
   <TR>
     <TD><% $old{$pre.'address2'} %></TD>
-    <TD><% $new{$pre.'address2'} %></TD>
   </TR>
   <TR>
     <TD><% $old{$pre.'city'} %>, <% $old{$pre.'state'} %>  <% $old{$pre.'zip'} %></TD>
-    <TD><% $new{$pre.'city'} %>, <% $new{$pre.'state'} %>  <% $new{$pre.'zip'} %></TD>
   </TR>
-
-%   } # if addr_clean
-%     elsif ( $new{$pre.'error'} ) {
+%   } else { # not an error
   <TR>
     <TH>Entered <%$name%> address</TH>
+    <TH>Standardized <%$name%> address</TH>
+  </TR>
+%   if ( !$new{$pre.'addr_clean'} ) {
+  <TR>
+    <TD></TD>
+    <TH STYLE="font-size:smaller;color:#ff0000">(unverified)</TH>
   </TR>
+%   }
+  <TR>
 %     if ( $old{$pre.'company'} ) {
   <TR>
     <TD><% $old{$pre.'company'} %></TD>
+    <TD><% $new{$pre.'company'} %></TD>
   </TR>
 %     }
   <TR>
     <TD><% $old{$pre.'address1'} %></TD>
-    <TD ROWSPAN=3><FONT COLOR="#ff0000"><B><% $new{$pre.'error'} %></B></FONT></TD>
+    <TD><% $new{$pre.'address1'} %></TD>
   </TR>
   <TR>
     <TD><% $old{$pre.'address2'} %></TD>
+    <TD><% $new{$pre.'address2'} %></TD>
   </TR>
   <TR>
     <TD><% $old{$pre.'city'} %>, <% $old{$pre.'state'} %>  <% $old{$pre.'zip'} %></TD>
+    <TD><% $new{$pre.'city'} %>, <% $new{$pre.'state'} %>  <% $new{$pre.'zip'} %></TD>
   </TR>
-%   } #if error
+
+%   } # if error
 % } # for $pre
 
 %# only do this part if address standardization provided a censustract
@@ -88,7 +93,7 @@ Confirm address standardization
   </TR>
 % } #if censustract
 
-% if ( $new{bill_error} or $new{ship_error} ) {
+% if ( grep {$new{$_.'error'}} @prefixes ) {
   <TR>
     <TD ALIGN="center">
     <BUTTON TYPE="button" STYLE="width:205px" onclick="confirm_manual_address();">
@@ -99,8 +104,7 @@ Confirm address standardization
       <IMG SRC="<%$p%>images/cross.png" ALT=""> Cancel submission
     </BUTTON></TD>
   </TR>
-% }
-% else {
+% } else {
   <TR>
     <TD ALIGN="center">
     <BUTTON TYPE="button" STYLE="width:205px" onclick="confirm_manual_address();">
index 962ee51..7edf892 100644 (file)
@@ -54,6 +54,7 @@ unless ( $error ) { # if ($access_user) {
                       spreadsheet_format mobile_menu
                       enable_fuzzy_on_exact
                       disable_html_editor disable_enter_submit_onetimecharge
+                      enable_mask_clipboard_hack
                       email_address
                       snom-ip snom-username snom-password
                       vonage-fromnumber vonage-username vonage-password
index d2b8835..ccfeecd 100644 (file)
@@ -137,6 +137,13 @@ Interface
     </TD>
   </TR>
 
+  <TR>
+    <TH ALIGN="right">Don't copy MAC address delimiters to clipboard</TH>
+    <TD ALIGN="left" COLSPAN=2>
+      <INPUT TYPE="checkbox" NAME="enable_mask_clipboard_hack" VALUE="1" <% $curuser->option('enable_mask_clipboard_hack') ? 'CHECKED' : '' %>>
+    </TD>
+  </TR>
+
 </TABLE>
 <BR>
 
index 4c5e90f..d86641d 100644 (file)
@@ -144,8 +144,8 @@ Filtering parameters:
 
 - taxnum: Limit to items whose tax definition matches this taxnum.
   With "nottax" that means items that are subject to that tax;
-  with "istax" it's the tax charges themselves.  Can be specified 
-  more than once to include multiple taxes.
+  with "istax" it's the tax charges themselves.  Can be a comma-separated
+  list to include multiple taxes.
 
 - country, state, county, city: Limit to items whose tax location 
   matches these fields.  If "nottax" it's the tax location of the package;
@@ -283,24 +283,7 @@ if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
   push @where, "cust_main.agentnum = $1";
 }
 
-# salesnum
-if ( $cgi->param('salesnum') =~ /^(\d+)$/ ) {
-
-  my $salesnum = $1;
-
-  my $cmp_salesnum = $cgi->param('cust_main_sales')
-                       ? ' COALESCE( cust_pkg.salesnum, cust_main.salesnum )'
-                       : ' cust_pkg.salesnum ';
-
-  push @where, "$cmp_salesnum = $salesnum";
-
-  #because currently we're called from sales_pkg_class.html for a specific
-  # class (or empty class) but not for all classes
-  #will have to do something to distinguish if someone wants the sales report
-  # (report_cust_bill_pkg.html) to have a sales person dropdown
-  $cgi->param('classnum', 0) unless $cgi->param('classnum');
-}
-
+# salesnum--see below
 # refnum
 if ( $cgi->param('refnum') =~ /^(\d+)$/ ) {
   push @where, "cust_main.refnum = $1";
@@ -350,7 +333,7 @@ if ( $cgi->param('nottax') ) {
   # 0: empty class
   # N: classnum
   if ( grep { $_ eq 'classnum' } $cgi->param ) {
-    my @classnums = grep /^\d+$/, $cgi->param('classnum');
+    my @classnums = grep /^\d*$/, $cgi->param('classnum');
     push @where, "COALESCE($part_pkg.classnum, 0) IN ( ".
                      join(',', @classnums ).
                  ' )'
@@ -390,11 +373,8 @@ if ( $cgi->param('nottax') ) {
   # we don't handle exempt_monthly here
   
   if ( $cgi->param('taxname') ) { # specific taxname
-      push @tax_where, 'cust_main_county.taxname = '.
+      push @tax_where, "COALESCE(cust_main_county.taxname, 'Tax') = ".
                         dbh->quote($cgi->param('taxname'));
-  } elsif ( $cgi->param('taxnameNULL') ) {
-      push @tax_where, 'cust_main_county.taxname IS NULL OR '.
-                       'cust_main_county.taxname = \'Tax\'';
   }
 
   # country:state:county:city:district (may be repeated)
@@ -422,12 +402,8 @@ if ( $cgi->param('nottax') ) {
   }
 
   # specific taxnums
-  if ( $cgi->param('taxnum') ) {
-    my $taxnum_in = join(',', 
-      grep /^\d+$/, $cgi->param('taxnum')
-    );
-    push @tax_where, "cust_main_county.taxnum IN ($taxnum_in)"
-      if $taxnum_in;
+  if ( $cgi->param('taxnum') =~ /^([\d,]+)$/) {
+    push @tax_where, "cust_main_county.taxnum IN ($1)";
   }
 
   # If we're showing exempt items, we need to find those with 
@@ -457,22 +433,16 @@ if ( $cgi->param('nottax') ) {
     USING (billpkgnum)";
   }
  
-  if ( @tax_where or $cgi->param('taxable') or $cgi->param('out') ) { 
-    # process tax restrictions
-    unshift @tax_where,
-      'cust_main_county.tax > 0';
+  # process tax restrictions
+  unshift @tax_where,
+    'cust_bill_pkg_tax_location.taxable_billpkgnum = cust_bill_pkg.billpkgnum',
+    'cust_main_county.tax > 0';
 
-    my $tax_sub = "SELECT invnum, cust_bill_pkg_tax_location.pkgnum
+  my $tax_sub = "SELECT 1
     FROM cust_bill_pkg_tax_location
     JOIN cust_bill_pkg AS tax_item USING (billpkgnum)
     JOIN cust_main_county USING (taxnum)
-    WHERE ". join(' AND ', @tax_where).
-    " GROUP BY invnum, cust_bill_pkg_tax_location.pkgnum";
-
-    $join_pkg .= " LEFT JOIN ($tax_sub) AS item_tax
-    ON (item_tax.invnum = cust_bill_pkg.invnum AND
-        item_tax.pkgnum = cust_bill_pkg.pkgnum)";
-  }
+    WHERE ". join(' AND ', @tax_where);
 
   # now do something with that
   if ( @exempt_where ) {
@@ -489,23 +459,17 @@ if ( $cgi->param('nottax') ) {
     my $taxable = 'cust_bill_pkg.setup + cust_bill_pkg.recur '.
                   '- COALESCE(item_exempt.exempt_amount, 0)';
 
-    push @where,    'item_tax.invnum IS NOT NULL';
     push @select,   "($taxable) AS taxable_amount";
+    push @where,    "EXISTS($tax_sub)";
     push @peritem,  'taxable_amount';
     push @peritem_desc, 'Taxable';
     push @total,    "SUM($taxable)";
     push @total_desc, "$money_char%.2f taxable";
 
-  } elsif ( $cgi->param('out') ) {
-  
-    push @where,    'item_tax.invnum IS NULL',
-                    'item_exempt.billpkgnum IS NULL';
-
   } elsif ( @tax_where ) {
 
     # union of taxable + all exempt_ cases
-    push @where,
-      '(item_tax.invnum IS NOT NULL OR item_exempt.billpkgnum IS NOT NULL)';
+    push @where, "(EXISTS($tax_sub) OR item_exempt.billpkgnum IS NOT NULL)";
 
   }
 
@@ -572,6 +536,21 @@ if ( $cgi->param('nottax') ) {
     # don't double-count the components of consolidated taxes
     $total[0] = 'COUNT(DISTINCT cust_bill_pkg.billpkgnum)';
     $total[1] = 'SUM(cust_bill_pkg_tax_location.amount)';
+
+    # package classnum
+    if ( grep { $_ eq 'classnum' } $cgi->param ) {
+      my @classnums = grep /^\d*$/, $cgi->param('classnum');
+      $join_pkg .= '
+        JOIN cust_pkg AS taxed_pkg 
+          ON (cust_bill_pkg_tax_location.pkgnum = taxed_pkg.pkgnum)
+        JOIN part_pkg AS taxed_part_pkg
+          ON (taxed_pkg.pkgpart = taxed_part_pkg.pkgpart)
+      ';
+      push @where, "COALESCE(taxed_part_pkg.classnum, 0) IN ( ".
+                       join(',', @classnums ).
+                   ' )'
+        if @classnums;
+    }
   }
 
   # taxclass
@@ -589,25 +568,8 @@ if ( $cgi->param('nottax') ) {
   }
 
   # specific taxnums
-  if ( $cgi->param('taxnum') ) {
-    my $taxnum_in = join(',', 
-      grep /^\d+$/, $cgi->param('taxnum')
-    );
-    push @where, "cust_main_county.taxnum IN ($taxnum_in)"
-      if $taxnum_in;
-  }
-
-  # report group (itemdesc)
-  if ( $cgi->param('report_group') =~ /^(=|!=) (.*)$/ ) {
-    my ( $group_op, $group_value ) = ( $1, $2 );
-    if ( $group_op eq '=' ) {
-      #push @where, 'itemdesc LIKE '. dbh->quote($group_value.'%');
-      push @where, 'itemdesc = '. dbh->quote($group_value);
-    } elsif ( $group_op eq '!=' ) {
-      push @where, '( itemdesc != '. dbh->quote($group_value) .' OR itemdesc IS NULL )';
-    } else {
-      die "guru meditation #00de: group_op $group_op\n";
-    }
+  if ( $cgi->param('taxnum') =~ /^([\d,]+)$/) {
+    push @where, "cust_main_county.taxnum IN ($1)";
   }
 
   # itemdesc, for some reason
@@ -704,6 +666,28 @@ if ( $cgi->param('credit') ) {
 
 push @select, 'cust_main.custnum', FS::UI::Web::cust_sql_fields();
 
+#salesnum
+if ( $cgi->param('salesnum') =~ /^(\d+)$/ ) {
+
+  my $salesnum = $1;
+  my $sales = FS::sales->by_key($salesnum)
+    or die "salesnum $salesnum not found";
+
+  my $subsearch = $sales->cust_bill_pkg_search('', '',
+    'cust_main_sales' => ($cgi->param('cust_main_sales') ? 1 : 0),
+    'paid'            => ($cgi->param('paid') ? 1 : 0),
+    'classnum'        => scalar($cgi->param('classnum'))
+  );
+  $join_pkg .= " JOIN sales_pkg_class ON ( COALESCE(sales_pkg_class.classnum, 0) = COALESCE( part_pkg.classnum, 0) )";
+
+  my $extra_sql = $subsearch->{extra_sql};
+  $extra_sql =~ s/^WHERE//;
+  push @where, $extra_sql;
+
+  $cgi->param('classnum', 0) unless $cgi->param('classnum');
+}
+
+
 my $where = join(' AND ', @where);
 $where &&= "WHERE $where";
 
index f598341..6da5787 100644 (file)
@@ -7,6 +7,7 @@
                  'header'      => [
                    #'#',
                    'Discount',
+                   'Class',
                    'Amount',
                    'Months',
                    'Package',
@@ -17,6 +18,7 @@
                  'fields'      => [
                    #'billpkgdiscountnum',
                    sub { $_[0]->cust_pkg_discount->discount->description },
+                   sub { $_[0]->cust_pkg_discount->discount->classname },
                    sub { sprintf($money_char.'%.2f', shift->amount ) },
                    sub { my $m = shift->months;
                          $m =~ /\./ ? sprintf('%.2f', $m) : $m;
@@ -28,6 +30,7 @@
                  ],
                  'sort_fields' => [
                    '',
+                   '',
                    'amount',
                    'months',
                    'pkg',
@@ -40,6 +43,7 @@
                    '',
                    '',
                    '',
+                   '',
                    $ilink,
                    $ilink,
                    ( map { $_ ne 'Cust. Status' ? $clink : '' }
@@ -47,7 +51,7 @@
                    ),
                  ],
                  #'align' => 'rlrrrc'.FS::UI::Web::cust_aligns(),
-                 'align' => 'lrrlrr'.FS::UI::Web::cust_aligns(),
+                 'align' => 'lcrrlrr'.FS::UI::Web::cust_aligns(),
                  'color' => [ 
                               #'',
                               '',
@@ -56,6 +60,7 @@
                               '',
                               '',
                               '',
+                              '',
                               FS::UI::Web::cust_colors(),
                             ],
                  'style' => [ 
@@ -66,6 +71,7 @@
                               '',
                               '',
                               '',
+                              '',
                               FS::UI::Web::cust_styles(),
                             ],
            
@@ -98,7 +104,51 @@ if ( $cgi->param('usernum') =~ /^(\d+)$/ ) {
   push @where, "cust_pkg_discount.usernum = $1";
 }
 
-# #classnum
+# (discount) classnum
+my $join_discount = '';
+#false laziness w/cust_pkg_discount.html and cust_pkg.pm::search
+if ( grep { $_ eq 'discount_classnum' } $cgi->param ) {
+
+#  my @classnum = ();
+#  if ( ref($params->{'discount_classnum'}) ) {
+#
+#    if ( ref($params->{'discount_classnum'}) eq 'HASH' ) {
+#      @classnum = grep $params->{'discount_classnum'}{$_}, keys %{ $params->{'discount_classnum'} };
+#    } elsif ( ref($params->{'discount_classnum'}) eq 'ARRAY' ) {
+#      @classnum = @{ $params->{'discount_classnum'} };
+#    } else {
+#      die 'unhandled discount_classnum ref '. $params->{'discount_classnum'};
+#    }
+#
+#
+#  } elsif ( $params->{'discount_classnum'} =~ /^(\d*)$/ && $1 ne '0' ) {
+#    @classnum = ( $1 );
+#  }
+#
+#  if ( @classnum ) {
+
+   if ( $cgi->param('discount_classnum') =~ /^(\d*)$/ && $1 ne '0' ) {
+    my @classnum = ( $1 );
+
+    $join_discount = 'LEFT JOIN discount USING (discountnum)';
+
+    my @c_where = ();
+    my @nums = grep $_, @classnum;
+    push @c_where, 'discount.classnum IN ('. join(',',@nums). ')' if @nums;
+    my $null = scalar( grep { $_ eq '' } @classnum );
+    push @c_where, 'discount.classnum IS NULL' if $null;
+
+    if ( scalar(@c_where) == 1 ) {
+      push @where, @c_where;
+    } elsif ( @c_where ) {
+      push @where, ' ( '. join(' OR ', @c_where). ' ) ';
+    }
+
+  }
+
+}
+
+# #(package) classnum
 # # not specified: all classes
 # # 0: empty class
 # # N: classnum
@@ -121,7 +171,7 @@ if ( $cgi->param('usernum') =~ /^(\d+)$/ ) {
 #   }
 # }
 
-my $count_query = "SELECT COUNT(*), SUM(amount)";
+my $count_query = "SELECT COUNT(*), SUM(cust_bill_pkg_discount.amount)";
 
 my $join_cust_pkg_discount =
   'LEFT JOIN cust_pkg_discount USING (pkgdiscountnum)';
@@ -137,11 +187,11 @@ my $join_pkg =
   #LEFT JOIN part_pkg AS override
   #  ON pkgpart_override = override.pkgpart ';
 
+my $join = "$join_cust_pkg_discount $join_discount $join_pkg $join_cust";
+
 my $where = ' WHERE '. join(' AND ', @where);
 
-$count_query .=
-  " FROM cust_bill_pkg_discount $join_cust_pkg_discount $join_pkg $join_cust ".
-  $where;
+$count_query .= " FROM cust_bill_pkg_discount $join $where";
 
 my @select = (
                'cust_bill_pkg_discount.*',
@@ -155,7 +205,7 @@ push @select, 'cust_main.custnum',
 
 my $query = {
   'table'     => 'cust_bill_pkg_discount',
-  'addl_from' => "$join_cust_pkg_discount $join_pkg $join_cust",
+  'addl_from' => $join,
   'hashref'   => {},
   'select'    => join(', ', @select ),
   'extra_sql' => $where,
index 995779a..54bfa00 100755 (executable)
@@ -175,6 +175,10 @@ for my $param (qw( censustract censustract2 )) {
     if grep { $_ eq $param } $cgi->param;
 }
 
+#location flags (checkboxes)
+my @loc = grep /^\w+$/, $cgi->param('loc');
+$search_hash{"location_$_"} = 1 foreach @loc;
+
 my $report_option = $cgi->param('report_option');
 $search_hash{report_option} = $report_option if $report_option;
 
index 23af180..f0c7447 100644 (file)
@@ -6,6 +6,7 @@
                   #'redirect'    => $link,
                   'header'      => [ 'Status',
                                      'Discount',
+                                     'Class',
                                      'Months used',
                                      'Employee',
                                      'Package',
@@ -16,6 +17,7 @@
                   'fields'      => [
                                      sub { ucfirst( shift->status ) },
                                      sub { shift->discount->description },
+                                     sub { shift->discount->classname },
                                      sub { my $m = shift->months_used;
                                            $m =~ /\./ ? sprintf('%.2f',$m) : $m;
                                          },
                                      '',
                                      '',
                                      '',
+                                     '',
                                      ( map { $_ ne 'Cust. Status' ? $clink : ''}
                                            FS::UI::Web::cust_header()
                                      ),
                                    ],
-                  'align'       => 'clrll'. FS::UI::Web::cust_aligns(),
+                  'align'       => 'clcrll'. FS::UI::Web::cust_aligns(),
                   'color'       => [ 
                                      '',
                                      '',
                                      '',
                                      '',
                                      '',
+                                     '',
                                      FS::UI::Web::cust_colors(),
                                    ],
                  'style'        => [ 
@@ -48,6 +52,7 @@
                                      '',
                                      '',
                                      '',
+                                     '',
                                      FS::UI::Web::cust_styles(),
                                    ],
            
@@ -78,6 +83,47 @@ if ( $cgi->param('status') eq 'active' ) {
                ";     #XXX also end date
 }
 
+#classnum
+#false laziness w/cust_pkg.pm::search
+if ( grep { $_ eq 'classnum' } $cgi->param ) {
+
+#  my @classnum = ();
+#  if ( ref($params->{'classnum'}) ) {
+#
+#    if ( ref($params->{'classnum'}) eq 'HASH' ) {
+#      @classnum = grep $params->{'classnum'}{$_}, keys %{ $params->{'classnum'} };
+#    } elsif ( ref($params->{'classnum'}) eq 'ARRAY' ) {
+#      @classnum = @{ $params->{'classnum'} };
+#    } else {
+#      die 'unhandled classnum ref '. $params->{'classnum'};
+#    }
+#
+#
+#  } elsif ( $params->{'classnum'} =~ /^(\d*)$/ && $1 ne '0' ) {
+#    @classnum = ( $1 );
+#  }
+#
+#  if ( @classnum ) {
+
+   if ( $cgi->param('classnum') =~ /^(\d*)$/ && $1 ne '0' ) {
+    my @classnum = ( $1 );
+
+    my @c_where = ();
+    my @nums = grep $_, @classnum;
+    push @c_where, 'discount.classnum IN ('. join(',',@nums). ')' if @nums;
+    my $null = scalar( grep { $_ eq '' } @classnum );
+    push @c_where, 'discount.classnum IS NULL' if $null;
+
+    if ( scalar(@c_where) == 1 ) {
+      push @where, @c_where;
+    } elsif ( @c_where ) {
+      push @where, ' ( '. join(' OR ', @c_where). ' ) ';
+    }
+
+  }
+
+}
+
 #usernum
 if ( $cgi->param('usernum') =~ /^(\d+)$/ ) {
   push @where, "cust_pkg_discount.usernum = $1";
index 40b9ed7..382dbc4 100644 (file)
@@ -101,7 +101,7 @@ my $join = "
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('View customer tax exemptions');
 
-my @where = ("exempt_monthly = 'Y'");
+my @where = ( "exempt_monthly = 'Y'" );
 
 my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
 if ( $beginning || $ending ) {
@@ -118,28 +118,7 @@ if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
   push @where,  "cust_main.custnum = $1";
 }
 
-if ( $cgi->param('out') ) {
-  # wtf? how would you ever get exemptions on a non-taxable package location?
-
-  push @where, "
-    0 = (
-      SELECT COUNT(*) FROM cust_main_county AS county_out
-      WHERE (    county_out.county  = cust_main.county
-              OR ( county_out.county IS NULL AND cust_main.county  =  '' )
-              OR ( county_out.county  =  ''  AND cust_main.county IS NULL)
-              OR ( county_out.county IS NULL AND cust_main.county IS NULL)
-            )
-        AND (    county_out.state   = cust_main.state
-              OR ( county_out.state  IS NULL AND cust_main.state  =  ''  )
-              OR ( county_out.state   =  ''  AND cust_main.state IS NULL )
-              OR ( county_out.state  IS NULL AND cust_main.state IS NULL )
-            )
-        AND county_out.country = cust_main.country
-        AND county_out.tax > 0
-    )
-  ";
-
-} elsif ( $cgi->param('country' ) ) {
+if ( $cgi->param('country' ) ) {
 
   my $county  = dbh->quote( $cgi->param('county')  );
   my $state   = dbh->quote( $cgi->param('state')   );
@@ -150,11 +129,19 @@ if ( $cgi->param('out') ) {
   push @where, 'taxclass = '. dbh->quote( $cgi->param('taxclass') )
     if $cgi->param('taxclass');
 
-} elsif ( $cgi->param('taxnum') ) {
+}
+
+if ( $cgi->param('taxnum') ) {
 
-  my $taxnum_in = join(',', grep /^\d+$/, $cgi->param('taxnum') );
-  push @where, "taxnum IN ($taxnum_in)" if $taxnum_in;
+  my @taxnums = grep /^\d+$/, map { split(',', $_) } $cgi->param('taxnum');
+  if ( $cgi->param('taxnum') =~ /^([\d,]+)$/) {
+    push @where, "cust_tax_exempt_pkg.taxnum IN ($1)";
+  }
+
+}
 
+if ( $cgi->param('classnum') =~ /^(\d+)$/ ) {
+  push @where, "COALESCE(part_pkg.classnum,0) = $1";
 }
 
 my $where = scalar(@where) ? 'WHERE '.join(' AND ', @where) : '';
index f9ab901..77affd1 100644 (file)
@@ -1,30 +1,35 @@
-<% include('/elements/header.html', 'Discount report' ) %>
+<& /elements/header.html, 'Discount report' &>
 
 <FORM ACTION="cust_bill_pkg_discount.html" METHOD="GET">
 
 
 <TABLE>
 
-  <% include( '/elements/tr-select-user.html',
-                'label'       => 'Discounts by employee: ',
-                'access_user' => \%access_user,
-            )
-  %>
+  <& /elements/tr-select-discount_class.html,
+       'field'       => 'discount_classnum',
+       'pre_options' => [ '0' => 'all' ],
+       'empty_label' => '(none)'
+  &>
 
-  <% include( '/elements/tr-select-agent.html',
-                 'curr_value'    => scalar( $cgi->param('agentnum') ),
-                 'label'         => 'for agent: ',
-                 'disable_empty' => 0,
-             )
-  %>
+  <& /elements/tr-select-user.html,
+       'label'       => 'Discounts by employee: ',
+       'access_user' => \%access_user,
+  &>
 
-  <% include( '/elements/tr-input-beginning_ending.html' ) %>
+  <& /elements/tr-select-agent.html,
+       'curr_value'    => scalar( $cgi->param('agentnum') ),
+       'label'         => 'for agent: ',
+       'disable_empty' => 0,
+  &>
 
-  <% include( '/elements/tr-input-lessthan_greaterthan.html',
-                'label' => 'Amount',
-               'field' => 'amount',
-            )
-  %>
+  <& /elements/tr-input-beginning_ending.html &>
+
+<!-- doesn't actually work yet, needs support in cust_bill_pkg_discount.html
+  <& /elements/tr-input-lessthan_greaterthan.html,
+       'label' => 'Amount',
+       'field' => 'amount',
+  &>
+-->
 
 </TABLE>
 
@@ -33,7 +38,7 @@
 
 </FORM>
 
-<% include('/elements/footer.html') %>
+<& /elements/footer.html &>
 <%init>
 
 die "access denied"
index ebff7fa..1ceb48e 100755 (executable)
     &>
 
     <TR>
-      <TD ALIGN="right" VALIGN="center"><% mt('Without census tract') |h %></TD>
-        <TD><INPUT TYPE="checkbox" NAME="no_censustract"></TD>
-    </TR>
-
-%   if ( $conf->exists('enable_taxproducts') ) {
-
-      <TR>
-        <TD ALIGN="right" VALIGN="center"><% mt('With hardcoded tax location') |h %></TD>
-          <TD><INPUT TYPE="checkbox" NAME="with_geocode"></TD>
-      </TR>
-
-%   }
-
-    <TR>
       <TD ALIGN="right" VALIGN="center"><% mt('With email address(es)') |h %></TD>
         <TD><INPUT TYPE="checkbox" NAME="with_email"></TD>
     </TR>
index f9aabfc..e75a098 100755 (executable)
@@ -8,11 +8,7 @@
 
   <TABLE BGCOLOR="#cccccc" CELLSPACING=0>
 
-    <TR>
-      <TH CLASS="background" COLSPAN=2 ALIGN="left">
-        <FONT SIZE="+1">Customer search options</FONT>
-      </TH>
-    </TR>
+    <& /elements/tr-title.html, value => mt('Customer search options') &>
 
     <& /elements/tr-select-agent.html,
                    'curr_value'    => scalar( $cgi->param('agentnum') ),
 
   <TABLE BGCOLOR="#cccccc" CELLSPACING=0>
 
-    <TR>
-      <TH CLASS="background" COLSPAN=2 ALIGN="left">
-        <FONT SIZE="+1">Package search options</FONT>
-      </TH>
-    </TR>
+    <& /elements/tr-title.html, value => mt('Package search options') &>
 
     <& /elements/tr-select-sales.html,
                   'label'         => 'Package sales person',
                   'disable_empty' => 1,
     &>
 
-    <% include( '/elements/tr-select-cust_pkg-status.html',
+    <& /elements/tr-select-cust_pkg-status.html,
                   'label'    => 'Package status',
                   'onchange' => 'status_changed(this);',
-              )
-    %>
+    &>
 
     <SCRIPT TYPE="text/javascript">
   
 
     </SCRIPT>
 
-    <% include( '/elements/tr-select-pkg_class.html',
+    <& /elements/tr-select-pkg_class.html,
                    'pre_options' => [ '0' => 'all' ],
                    'empty_label' => '(empty class)',
-               )
-    %>
+    &>
 
 %   if ( scalar( qsearch( 'part_pkg_report_option', { 'disabled' => '' } ) ) ) {
 
-    <% include( '/elements/tr-select-table.html',
+    <& /elements/tr-select-table.html,
                    'label'        => 'Report classes',
                    'table'        => 'part_pkg_report_option',
                    'name_col'     => 'name',
                    'hashref'      => { 'disabled' => '' },
                    'element_name' => 'report_option',
                    'multiple'     => 'multiple',
-               )
-    %>
+    &>
 
 %   }
     <TR>
 
     </SCRIPT>
 
-    <% include( '/elements/tr-checkbox.html',
+    <& /elements/tr-checkbox.html,
                 'label' => 'Custom packages',
                 'field' => 'custom',
                 'value' => 1,
                 'onchange' => 'custom_changed(this);',
-              )
-    %> 
+    &> 
 
-    <% include( '/elements/tr-selectmultiple-part_pkg.html' ) %
+    <& /elements/tr-selectmultiple-part_pkg.html &
 
-    <TR>
-      <TH CLASS="background" COLSPAN=2>&nbsp;</TH>
-    </TR>
+    <& /elements/tr-title.html, value => mt('Location search options') &>
 
-    <TR>
-      <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Display options</FONT></TH>
-    </TR>
-    <% include( '/elements/tr-select-cust-fields.html' ) %>
+%   my @location_options = qw(cust nocust census nocensus);
+%   if ( $conf->exists('enable_taxproducts') ) {
+%     push @location_options, 'geocode', 'nogeocode';
+%   }
+    <& /elements/tr-checkbox-multiple.html,
+                'label'   => 'Where package location:',
+                'field'   => 'loc',
+                'options' => \@location_options,
+                'labels'  => { 'cust'     => "is the customer's default location",
+                               'nocust'   => "is not the customer's default location",
+                               'census'   => "has a census tract",
+                               'nocensus' => "does not have a census tract",
+                               'nogeocode'=> 'has an implicit tax location',
+                               'geocode'  => 'has a hardcoded tax location',
+                             },
+                'value'   => { map { $_ => 1 } @location_options },
+    &>
+
+    <& /elements/tr-title.html, value => mt('Display options') &>
+
+    <& /elements/tr-select-cust-fields.html &>
     
   </TABLE>
 
@@ -276,4 +279,5 @@ my %checkbox = (
   'cancel'    => 1,
 );
 
+my $conf = FS::Conf->new;
 </%once>
index 31774c3..7f0e55f 100644 (file)
@@ -1,4 +1,4 @@
-<% include('/elements/header.html', 'Package discount report' ) %>
+<& /elements/header.html, 'Package discount report' &>
 
 <FORM ACTION="cust_pkg_discount.html" METHOD="GET">
 
@@ -6,7 +6,7 @@
 <TABLE>
 
   <TR>
-    <TD>Discount status</TD>
+    <TD ALIGN="right">Discount status</TD>
     <TD>
       <SELECT NAME="status">
         <OPTION VALUE="active">Active
     </TD>
   </TR>
 
-  <% include( '/elements/tr-select-user.html',
-                'label'       => 'Discounts by employee: ',
-                'access_user' => \%access_user,
-            )
-  %>
+  <& /elements/tr-select-discount_class.html,
+       'pre_options' => [ '0' => 'all' ],
+       'empty_label' => '(none)'
+  &>
 
-  <% include( '/elements/tr-select-agent.html',
-                 'curr_value'    => scalar( $cgi->param('agentnum') ),
-                 'label'         => 'for agent: ',
-                 'disable_empty' => 0,
-             )
-  %>
+  <& /elements/tr-select-user.html,
+       'label'       => 'Discounts by employee: ',
+       'access_user' => \%access_user,
+  &>
+
+  <& /elements/tr-select-agent.html,
+       'curr_value'    => scalar( $cgi->param('agentnum') ),
+       'label'         => 'for agent: ',
+       'disable_empty' => 0,
+  &>
 
 </TABLE>
 
@@ -36,7 +39,7 @@
 
 </FORM>
 
-<% include('/elements/footer.html') %>
+<& /elements/footer.html &>
 <%init>
 
 die "access denied"
index cc17e6b..19af428 100644 (file)
@@ -4,30 +4,47 @@
 
 <TABLE BGCOLOR="#cccccc" CELLSPACING=0>
 
-<& /elements/tr-select-agent.html,
-     'onchange'      => 'agent_changed(this)',
-&>
+% if ( $curuser->report_salesnum ) {
+
+    <INPUT TYPE="hidden" NAME="agentnum" VALUE="<% $curuser->report_sales->agentnum %>">
+    <INPUT TYPE="hidden" NAME="salesnum" VALUE="<% $curuser->report_salesnum %>">
+
+% } else {
+
+    <& /elements/tr-select-agent.html,
+         'onchange'      => 'agent_changed(this)',
+    &>
 
-<SCRIPT TYPE="text/javascript">
+    <SCRIPT TYPE="text/javascript">
 
-  function agent_changed(what) {
-    salesnum_agentnum_changed(what);
-  }
+      function agent_changed(what) {
+        salesnum_agentnum_changed(what);
+      }
 
-  <&| /elements/onload.js &>
-  agent_changed(document.getElementById('agentnum'))
-  </&>
+      <&| /elements/onload.js &>
+      agent_changed(document.getElementById('agentnum'))
+      </&>
 
-</SCRIPT>
+    </SCRIPT>
 
-<& /elements/tr-select-sales.html &>
+    <& /elements/tr-select-sales.html,
+        'empty_label' => 'all',
+    &>
+
+% }
 
 <& /elements/tr-checkbox.html,
-     'label' => 'Customer sales person if there is no package sales person',
-     'field' => 'cust_main_sales',
-     'value' => 'Y',
+    'label' => 'Customer sales person if there is no package sales person',
+    'field' => 'cust_main_sales',
+    'value' => 'Y',
 &>
 
+<& /elements/tr-checkbox.html,
+    'label' => 'Show paid sales only',
+    'field' => 'paid',
+    'value' => 'Y',
+&> 
+
 <& /elements/tr-input-beginning_ending.html &>
 
 </TABLE>
@@ -38,7 +55,8 @@
 <% include('/elements/footer.html') %>
 <%init>
 
-die "access denied"
-  unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied" unless $curuser->access_right('Financial reports');
 
 </%init>
index d71fcf9..111f22d 100755 (executable)
-<% include("/elements/header.html", "$agentname Tax Report - ".
-              ( $beginning
-                  ? time2str('%h %o %Y ', $beginning )
-                  : ''
-              ).
-              'through '.
-              ( $ending == 4294967295
-                  ? 'now'
-                  : time2str('%h %o %Y', $ending )
-              )
-          )
-%>
+<& /elements/header.html, "$agentname Tax Report: ".
+  ( $beginning
+      ? time2str('%h %o %Y ', $beginning )
+      : ''
+  ).
+  'through '.
+  ( $ending == 4294967295
+      ? 'now'
+      : time2str('%h %o %Y', $ending )
+  ). ' - ' . $taxname
+&>
 <TD ALIGN="right">
 Download full results<BR>
 as <A HREF="<% $p.'search/report_tax-xls.cgi?'.$cgi->query_string%>">Excel spreadsheet</A>
 </TD>
 
 <STYLE type="text/css">
-td.sectionhead {
+TD.sectionhead {
   background-color: #777777;
   color: #ffffff;
   font-weight: bold;
   text-align: left;
 }
+.grid TH { background-color: #cccccc; padding: 0px 3px 2px }
+.row0 TD { background-color: #eeeeee; padding: 0px 3px 2px; text-align: right}
+.row1 TD { background-color: #ffffff; padding: 0px 3px 2px; text-align: right}
+TD.rowhead { font-weight: bold; text-align: left }
+.bigmath { font-size: large; font-weight: bold; font: sans-serif; text-align: center }
 </STYLE>
-<% include('/elements/table-grid.html') %>
+<& /elements/table-grid.html &>
   <TR>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=9>Sales</TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3>Rate</TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3>Tax owed</TH>
-% unless ( $cgi->param('show_taxclasses') ) { 
-      <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3>Tax invoiced</TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3></TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3>Tax credited</TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3></TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3>Tax collected</TH>
-% } 
+    <TH ROWSPAN=3></TH>
+    <TH COLSPAN=5>Sales</TH>
+    <TH ROWSPAN=3></TH>
+    <TH ROWSPAN=3>Rate</TH>
+    <TH ROWSPAN=3></TH>
+    <TH ROWSPAN=3>Estimated tax</TH>
+    <TH ROWSPAN=3>Tax invoiced</TH>
+    <TH ROWSPAN=3></TH>
+    <TH ROWSPAN=3>Tax credited</TH>
+    <TH ROWSPAN=3></TH>
+    <TH ROWSPAN=3>Net tax due</TH>
   </TR>
 
   <TR>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2>Total</TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=1>Non-taxable</TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=1>Non-taxable</TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=1>Non-taxable</TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2>Taxable</TH>
+    <TH ROWSPAN=2>Total</TH>
+    <TH ROWSPAN=1>Non-taxable</TH>
+    <TH ROWSPAN=1>Non-taxable</TH>
+    <TH ROWSPAN=1>Non-taxable</TH>
+    <TH ROWSPAN=2>Taxable</TH>
   </TR>
 
-  <TR>
-    <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>(tax-exempt customer)</FONT></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>(tax-exempt package)</FONT></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>(monthly exemption)</FONT></TH>
+  <TR STYLE="font-size:small">
+    <TH>(tax-exempt customer)</TH>
+    <TH>(tax-exempt package)</TH>
+    <TH>(monthly exemption)</TH>
   </TR>
 
-% foreach my $class (@pkgclasses ) {
-%   next if @{ $class->{regions} } == 0;
-%   if ( $class->{classname} ) {
-  <TR>
-    <TD COLSPAN=19 CLASS="sectionhead"><% $class->{classname} %></TD>
+% my $row = 0;
+% my $classlink = '';
+% my $descend;
+% $descend = sub {
+%   my ($data, $label) = @_;
+%   if ( ref $data eq 'ARRAY' ) {
+%     # then we've reached the bottom
+%     my (%taxnums, %values);
+%     foreach (@$data) {
+%       $taxnums{ $_->[0] } = $_->[1];
+%       $values{ $_->[0] } = $_->[2];
+%     }
+%     # finally, output
+  <TR CLASS="row<% $row %>">
+%     # Row label
+    <TD CLASS="rowhead"><% $label |h %></TD>
+%     # Total Sales
+%     my $sales = $money_sprintf->(
+%       $values{taxable} +
+%       $values{exempt_cust} +
+%       $values{exempt_pkg} +
+%       $values{exempt_monthly}
+%     );
+%     my %sales_taxnums;
+%     foreach my $x (qw(taxable exempt_cust exempt_pkg exempt_monthly)) {
+%       foreach (split(',', $taxnums{$x})) {
+%         $sales_taxnums{$_} = 1;
+%       }
+%     }
+%     my $sales_taxnums = join(',', keys %sales_taxnums);
+    <TD>
+      <A HREF="<% "$saleslink;$classlink;taxnum=$sales_taxnums" %>">
+        <% $sales %>
+      </A>
+    </TD>
+%     # exemptions
+%     foreach(qw(cust pkg)) {
+    <TD>
+      <A HREF="<% "$saleslink;$classlink;exempt_$_=Y;taxnum=".$taxnums{"exempt_$_"} %>">
+        <% $money_sprintf->($values{"exempt_$_"}) %>
+      </A>
+    </TD>
+%     }
+    <TD>
+      <A HREF="<% "$exemptlink;$classlink;taxnum=".$taxnums{"exempt_monthly"} %>">
+        <% $money_sprintf->($values{"exempt_monthly"}) %>
+      </A>
+    </TD>
+%     # taxable
+    <TD>
+      <A HREF="<% "$saleslink;$classlink;taxable=1;taxnum=$taxnums{taxable}" %>">
+        <% $money_sprintf->($values{taxable}) %>
+      </A>
+    </TD>
+%     # tax rate
+%     my $rate;
+%     foreach(split(',', $taxnums{tax})) {
+%       $rate ||= $taxrates{$_};
+%       if ($rate != $taxrates{$_}) {
+%         $rate = 'variable';
+%         last;
+%       }
+%     }
+%     $rate = sprintf('%.2f', $rate) . '%' if ($rate and $rate ne 'variable');
+    <TD CLASS="bigmath"> &times; </TD>
+    <TD><% $rate %></TD>
+%     # estimated tax
+    <TD CLASS="bigmath"> = </TD>
+    <TD><% $rate eq 'variable' 
+            ? ''
+            : $money_sprintf->( $values{taxable} * $rate / 100 ) %>
+    </TD>
+%     # invoiced tax
+    <TD>
+      <A HREF="<% "$taxlink;$classlink;taxnum=$taxnums{taxable}" %>">
+        <% $money_sprintf->( $values{tax} ) %>
+      </A>
+    </TD>
+%     # credited tax
+    <TD CLASS="bigmath"> &minus; </TD>
+    <TD>
+      <A HREF="<% "$creditlink;$classlink;taxnum=$taxnums{credited}" %>">
+        <% $money_sprintf->( $values{credited} ) %>
+      </A>
+    </TD>
+%     # net tax due
+    <TD CLASS="bigmath"> = </TD>
+    <TD><% $money_sprintf->( $values{tax} - $values{credited} ) %></TD>
   </TR>
-%   }
 
-% my $bgcolor1 = '#eeeeee';
-% my $bgcolor2 = '#ffffff';
-% my $bgcolor;
-
-% my @regions = @{ $class->{regions} };
-% foreach my $region ( @regions ) {
-%
-%   my $link = '';
-%   if ( $with_pkgclass and length($class->{classnum}) ) {
-%     $link = ';classnum='.$class->{classnum};
-%   } # else we're not breaking down pkg class, or this is the grand total
-%
-%   if ( $region->{'label'} eq $out ) {
-%     $link .= ';out=1';
-%   } elsif ( $region->{'taxnums'} ) {
-%     # might be nicer to specify this as country:state:city
-%     $link .= ';'.join(';', map { "taxnum=$_" } @{ $region->{'taxnums'} });
-%   }
-%
-%   if ( $bgcolor eq $bgcolor1 ) {
-%     $bgcolor = $bgcolor2;
-%   } else {
-%     $bgcolor = $bgcolor1;
-%   }
+%     $row = $row ? 0 : 1;
 %
-%   my $hicolor = $bgcolor;
-%   unless ( $cgi->param('show_taxclasses') ) {
-%     my $diff = abs(   sprintf( '%.2f', $region->{'owed'} )
-%                     - sprintf( '%.2f', $region->{'tax'}  )
-%                   );
-%     if ( $diff > 0.02 ) {
-%       $hicolor = $hicolor eq '#eeeeee' ? '#eeee99' : '#ffffcc';
+%   } else { # we're not at the lowest classification
+%     my @keys = sort { $a <=> $b or $a cmp $b } keys(%$data);
+%     foreach my $key (@keys) {
+%       my $sublabel = join(', ', grep $_, $label, $key);
+%       &{ $descend }($data->{$key}, $sublabel);
 %     }
 %   }
-%
-%
-%   my $td = qq(TD CLASS="grid" BGCOLOR="$bgcolor");
-%   my $tdh = qq(TD CLASS="grid" BGCOLOR="$hicolor");
-%   my $bigmath = '<FONT FACE="sans-serif" SIZE="+1"><B>';
-%   my $bme = '</B></FONT>';
-
-%   if ( $region->{'is_total'} ) {
-    <TR STYLE="font-style: italic">
-      <TD STYLE="text-align: right; padding-right: 1ex; background-color:<%$bgcolor%>">Total</TD>
-%   } else {
-    <TR>
-      <<%$td%>><% $region->{'label'} %></TD>
+% };
+
+% my @pkgclasses = sort { $a <=> $b } keys %data;
+% foreach my $pkgclass (@pkgclasses) {
+%   my $class = FS::pkg_class->by_key($pkgclass) ||
+%               FS::pkg_class->new({ classname => 'Unclassified' });
+  <TBODY>
+%   if ( $breakdown{pkgclass} ) {
+  <TR>
+    <TD COLSPAN=19 CLASS="sectionhead"><% $class->classname %></TD>
+  </TR>
 %   }
-      <<%$td%> ALIGN="right">
-        <A HREF="<% $baselink. $link %>;nottax=1"
-        ><% &$money_sprintf( $region->{'sales'} ) %></A>
-      </TD>
-%   if ( $region->{'label'} eq $out ) {
-      <<%$td%> COLSPAN=12></TD>
-%   } else { #not $out
-      <<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD>
-      <<%$td%> ALIGN="right">
-        <A HREF="<% $baselink. $link %>;nottax=1;exempt_cust=Y"
-        ><% &$money_sprintf( $region->{'exempt_cust'} ) %></A>
-      </TD>
-      <<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD>
-      <<%$td%> ALIGN="right">
-        <A HREF="<% $baselink. $link %>;nottax=1;exempt_pkg=Y"
-        ><% &$money_sprintf( $region->{'exempt_pkg'} ) %></A>
-      </TD>
-      <<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD>
-      <<%$td%> ALIGN="right">
-        <A HREF="<% $exemptlink. $link %>"
-        ><% &$money_sprintf( $region->{'exempt_monthly'} ) %></A>
-        </TD>
-      <<%$td%>><FONT SIZE="+1"><B> = </B></FONT></TD>
-      <<%$td%> ALIGN="right">
-        <A HREF="<% $baselink. $link %>;nottax=1;taxable=1"
-        ><% &$money_sprintf( $region->{'taxable'} ) %></A>
-      </TD>
-      <<%$td%>><% $region->{'label'} eq 'Total' ? '' : "$bigmath X $bme" %></TD>
-      <<%$td%> ALIGN="right"><% $region->{'rate'} %></TD>
-      <<%$td%>><% $region->{'label'} eq 'Total' ? '' : "$bigmath = $bme" %></TD>
-      <<%$tdh%> ALIGN="right">
-        <% &$money_sprintf( $region->{'owed'} ) %>
-      </TD>
-%   } # if !$out
-%   unless ( $cgi->param('show_taxclasses') ) {
-%     my $invlink = $region->{'url_param_inv'}
-%                       ? ';'. $region->{'url_param_inv'}
-%                       : $link;
-
-%     if ( $region->{'label'} eq $out ) {
-        <<%$td%> ALIGN="right">
-          <A HREF="<% $baselink. $invlink %>;istax=1"
-          ><% &$money_sprintf_nonzero( $region->{'tax'} ) %></A>
-        </TD>
-        <<%$td%>></TD>
-        <<%$td%> ALIGN="right">
-          <A HREF="<% $creditlink. $invlink %>;istax=1"
-          ><% &$money_sprintf_nonzero( $region->{'credit'} ) %></A>
-        </TD>
-        <<%$td%> COLSPAN=2></TD>
-%     } else { #not $out
-        <<%$tdh%> ALIGN="right">
-          <A HREF="<% $baselink. $invlink %>;istax=1"
-          ><% &$money_sprintf( $region->{'tax'} ) %></A>
-        </TD>
-        <<%$tdh%>><FONT SIZE="+1"><B> - </B></FONT></TD>
-        <<%$tdh%> ALIGN="right">
-          <A HREF="<% $creditlink. $invlink %>;istax=1"
-          ><% &$money_sprintf( $region->{'credit'} ) %></A>
-        </TD>
-        <<%$tdh%>><FONT SIZE="+1"><B> = </B></FONT></TD>
-        <<%$tdh%> ALIGN="right">
-          <% &$money_sprintf( $region->{'tax'} - $region->{'credit'} ) %>
-        </TD>
-%     }
-%   } # show_taxclasses
-
-    </TR>
-% } # foreach $region
-
-%} # foreach $class
-
+%   $row = 0;
+%   $classlink = "classnum=".($pkgclass || 0) if $breakdown{pkgclass};
+%   &{ $descend }( $data{$pkgclass}, '' );
+%   # and now totals
+  </TBODY>
+  <TBODY CLASS="total">
+%   &{ $descend }( $total{$pkgclass}, 'Total' );
+  </TBODY>
+% } # foreach $pkgclass
 </TABLE>
 
-% if ( $cgi->param('show_taxclasses') ) {
-
-    <BR>
-    <% include('/elements/table-grid.html') %>
-    <TR>
-      <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc">Tax invoiced</TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc">Tax credited</TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc">Tax collected</TH>
-    </TR>
-
-%   #some false laziness w/above
-%   foreach my $class (@pkgclasses) {
-%   if ( $class->{classname} ) {
-    <TR>
-      <TD COLSPAN=6 CLASS="sectionhead"><% $class->{classname} %></TD>
-    </TR>
-%   }
-
-%   my $bgcolor1 = '#eeeeee';
-%   my $bgcolor2 = '#ffffff';
-%   my $bgcolor;
-%
-%   foreach my $region ( @{ $class->{base_regions} } ) {
-%
-%     my $link = '';
-%     if ( $with_pkgclass and length($class->{classnum}) ) {
-%       $link = ';classnum='.$class->{classnum};
-%     }
-%
-%     if ( $region->{'label'} eq $out ) {
-%       $link .= ';out=1';
-%     } else {
-%       $link .= ';'. $region->{'url_param'}
-%         if $region->{'url_param'};
-%     }
-%
-%     if ( $bgcolor eq $bgcolor1 ) {
-%       $bgcolor = $bgcolor2;
-%     } else {
-%       $bgcolor = $bgcolor1;
-%     }
-%     my $td = qq(TD CLASS="grid" BGCOLOR="$bgcolor");
-%     my $tdh = qq(TD CLASS="grid" BGCOLOR="$bgcolor");
-%
-%     #?
-%     my $invlink = $region->{'url_param_inv'}
-%                     ? ';'. $region->{'url_param_inv'}
-%                     : $link;
-
-      <TR>
-        <<%$td%>><% $region->{'label'} %></TD>
-%     if ( $region->{'label'} eq $out ) {
-        <<%$td%> ALIGN="right">
-          <A HREF="<% $baselink. $invlink %>;istax=1"
-          ><% &$money_sprintf_nonzero( $region->{'tax'} ) %></A>
-        </TD>
-        <<%$td%>></TD>
-        <<%$td%> ALIGN="right">
-          <A HREF="<% $creditlink. $invlink %>;istax=1"
-          ><% &$money_sprintf_nonzero( $region->{'credit'} ) %></A>
-        </TD>
-        <<%$td%> COLSPAN=2></TD>
-%     } else { #not $out
-        <<%$td%> ALIGN="right">
-          <A HREF="<% $baselink. $link %>;istax=1"
-          ><% &$money_sprintf( $region->{'tax'} ) %></A>
-        </TD>
-        <<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD>
-        <<%$tdh%> ALIGN="right">
-          <A HREF="<% $creditlink. $invlink %>;istax=1"
-          ><% &$money_sprintf( $region->{'credit'} ) %></A>
-        </TD>
-        <<%$td%>><FONT SIZE="+1"><B> = </B></FONT></TD>
-        <<%$tdh%> ALIGN="right">
-          <% &$money_sprintf( $region->{'tax'} - $region->{'credit'} ) %>
-        </TD>
-      </TR>
-%     } # if $out
-%   } #foreach $region
-% } #foreach $class
-
-  </TABLE>
-
-% } # if show_taxclasses
-
-<% include('/elements/footer.html') %>
-
+<& /elements/footer.html &>
 <%init>
 
 die "access denied"
@@ -287,17 +188,38 @@ my $DEBUG = $cgi->param('debug') || 0;
 
 my $conf = new FS::Conf;
 
-my $out = 'Out of taxable region(s)';
+my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
 
-my %label_opt = ( out => 1 ); #enable 'Out of Taxable Region' label
-$label_opt{with_city} = 1     if $cgi->param('show_cities');
-$label_opt{with_district} = 1 if $cgi->param('show_districts');
+my ($taxname, $country, %breakdown);
 
-$label_opt{with_taxclass} = 1 if $cgi->param('show_taxclasses');
+if ( $cgi->param('taxname') =~ /^([\w\s]+)$/ ) {
+  $taxname = $1;
+} else {
+  die "taxname required"; # UI prevents this
+}
 
-my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
+if ( $cgi->param('country') =~ /^(\w\w)$/ ) {
+  $country = $1;
+} else {
+  die "country required";
+}
 
-my $join_cust =     '     JOIN cust_bill      USING ( invnum  ) 
+# %breakdown: short name => field identifier
+foreach ($cgi->param('breakdown')) {
+  if ( $_ eq 'taxclass' ) {
+    $breakdown{'taxclass'} = 'part_pkg.taxclass';
+  } elsif ( $_ eq 'pkgclass' ) {
+    $breakdown{'pkgclass'} = 'part_pkg.classnum';
+  } elsif ( $_ eq 'city' ) {
+    $breakdown{'city'} = 'cust_main_county.city';
+    $breakdown{'district'} = 'cust_main_county.district';
+  }
+}
+# always break these down
+$breakdown{'state'} = 'cust_main_county.state';
+$breakdown{'county'} = 'cust_main_county.county';
+
+my $join_cust =     '      JOIN cust_bill     USING ( invnum  )
                       LEFT JOIN cust_main     USING ( custnum ) ';
 
 my $join_cust_pkg = $join_cust.
@@ -306,7 +228,7 @@ my $join_cust_pkg = $join_cust.
 
 my $from_join_cust_pkg = " FROM cust_bill_pkg $join_cust_pkg "; 
 
-my $with_pkgclass = $cgi->param('show_pkgclasses');
+# all queries MUST be linked to both cust_bill and cust_main_county
 
 # either or both of these can be used to link cust_bill_pkg to cust_main_county
 my $pkg_tax = "SELECT SUM(amount) as tax_amount, invnum, taxnum, ".
@@ -317,21 +239,32 @@ my $pkg_tax = "SELECT SUM(amount) as tax_amount, invnum, taxnum, ".
 my $pkg_tax_exempt = "SELECT SUM(amount) AS exempt_charged, billpkgnum, taxnum ".
   "FROM cust_tax_exempt_pkg EXEMPT_WHERE GROUP BY billpkgnum, taxnum";
 
-my $where = "WHERE _date >= $beginning AND _date <= $ending ";
+my $where = "WHERE _date >= $beginning AND _date <= $ending ".
+            "AND COALESCE(cust_main_county.taxname,'Tax') = '$taxname' ".
+            "AND cust_main_county.country = '$country'";
 # SELECT/GROUP clauses for first-level queries
-# classnum is a placeholder; they all go in one class in this case.
-my $select = "SELECT NULL AS classnum, cust_main_county.taxnum, ";
-my $group =  "GROUP BY cust_main_county.taxnum";
+my $select = "SELECT ";
+my $group = "GROUP BY ";
+foreach (qw(pkgclass taxclass state county city district)) {
+  if ( $breakdown{$_} ) {
+    $select .= "$breakdown{$_} AS $_, ";
+    $group  .= "$breakdown{$_}, ";
+  } else {
+    $select .= "NULL AS $_, ";
+  }
+}
+$select .= "array_to_string(array_agg(DISTINCT(cust_main_county.taxnum)), ',') AS taxnums, ";
+$group =~ s/, $//;
+
 # SELECT/GROUP clauses for second-level (totals) queries
-my $select_all = "SELECT NULL AS classnum, ";
-my $group_all =  "";
-
-if ( $with_pkgclass ) {
-  $select = "SELECT COALESCE(part_pkg.classnum,0), cust_main_county.taxnum, ";
-  $group =  "GROUP BY part_pkg.classnum, cust_main_county.taxnum";
-  $select_all = "SELECT COALESCE(part_pkg.classnum,0), ";
-  $group_all  = "GROUP BY COALESCE(part_pkg.classnum,0)";
+# breakdown by package class only, if anything
+my $select_all = "SELECT NULL AS pkgclass, ";
+my $group_all = "";
+if ( $breakdown{pkgclass} ) {
+  $select_all = "SELECT $breakdown{pkgclass} AS pkgclass, ";
+  $group_all = "GROUP BY $breakdown{pkgclass}";
 }
+$select_all .= "array_to_string(array_agg(DISTINCT(cust_main_county.taxnum)), ',') AS taxnums, ";
 
 my $agentname = '';
 if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
@@ -356,7 +289,8 @@ my $exempt = "$select SUM(exempt_charged)
   JOIN ($pkg_tax_exempt) AS pkg_tax_exempt
   USING (taxnum)
   JOIN cust_bill_pkg USING (billpkgnum)
-  $join_cust_pkg $where AND $nottax $group";
+  $join_cust_pkg $where AND $nottax
+  $group";
 
 my $all_exempt = "$select_all SUM(exempt_charged)
   FROM cust_main_county
@@ -393,24 +327,19 @@ $sql{taxable} = "$select
   LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt
     ON (pkg_tax_exempt.billpkgnum = cust_bill_pkg.billpkgnum 
         AND pkg_tax_exempt.taxnum = cust_main_county.taxnum)
-  $join_cust_pkg $where AND $nottax $group";
+  $join_cust_pkg $where AND $nottax 
+  $group";
 
-# Here we're going to sum all line items that are taxable _at all_,
-# under any tax.  exempt_charged is the sum of all exemptions for a 
-# particular billpkgnum + taxnum; we take the taxnum that has the 
-# smallest sum of exemptions and subtract that from the charged amount.
 $all_sql{taxable} = "$select_all
-  SUM(cust_bill_pkg.setup + cust_bill_pkg.recur - COALESCE(min_exempt, 0))
-  FROM cust_bill_pkg
-  JOIN (
-    SELECT invnum, pkgnum, MIN(exempt_charged) AS min_exempt
-    FROM ($pkg_tax) AS pkg_tax
-    JOIN cust_bill_pkg USING (invnum, pkgnum)
-    LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt USING (billpkgnum, taxnum)
-    GROUP BY invnum, pkgnum
-  ) AS pkg_is_taxable 
-  USING (invnum, pkgnum)
-  $join_cust_pkg $where AND $nottax $group_all";
+  SUM(cust_bill_pkg.setup + cust_bill_pkg.recur - COALESCE(exempt_charged, 0))
+  FROM cust_main_county
+  JOIN ($pkg_tax) AS pkg_tax USING (taxnum)
+  JOIN cust_bill_pkg USING (invnum, pkgnum)
+  LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt
+    ON (pkg_tax_exempt.billpkgnum = cust_bill_pkg.billpkgnum 
+        AND pkg_tax_exempt.taxnum = cust_main_county.taxnum)
+  $join_cust_pkg $where AND $nottax 
+  $group_all";
 
 $sql{taxable} =~ s/EXEMPT_WHERE//; # unrestricted
 $all_sql{taxable} =~ s/EXEMPT_WHERE//;
@@ -428,7 +357,7 @@ my $taxfrom = " FROM cust_bill_pkg
                 LEFT JOIN cust_bill_pkg_tax_location USING ( billpkgnum )
                 LEFT JOIN cust_main_county USING ( taxnum )";
 
-if ( $with_pkgclass ) {
+if ( $breakdown{pkgclass} ) {
   # If we're not grouping by package class, this is unnecessary, and
   # probably really expensive.
   $taxfrom .= "
@@ -439,17 +368,14 @@ if ( $with_pkgclass ) {
 }
 
 my $istax = "cust_bill_pkg.pkgnum = 0";
-my $named_tax =
-  "COALESCE(taxname,'Tax') = COALESCE(cust_bill_pkg.itemdesc,'Tax')";
 
 $sql{tax} = "$select SUM(cust_bill_pkg_tax_location.amount)
              $taxfrom
-             $where AND $istax AND $named_tax
+             $where AND $istax
              $group";
 
-$all_sql{tax} = "$select_all SUM(cust_bill_pkg.setup)
-             FROM cust_bill_pkg
-             $join_cust
+$all_sql{tax} = "$select_all SUM(cust_bill_pkg_tax_location.amount)
+             $taxfrom
              $where AND $istax
              $group_all";
 
@@ -463,325 +389,86 @@ my $creditwhere = $where .
 
 $sql{credit} = "$select SUM(cust_credit_bill_pkg.amount)
                 $creditfrom
-                $creditwhere AND $istax AND $named_tax
+                $creditwhere AND $istax
                 $group";
 
 $all_sql{credit} = "$select_all SUM(cust_credit_bill_pkg.amount)
-                FROM cust_credit_bill_pkg
-                JOIN cust_bill_pkg USING (billpkgnum)
-                $join_cust
-                $where AND $istax
+                $creditfrom
+                $creditwhere AND $istax
                 $group_all";
 
-if ( $with_pkgclass ) {
-  # the slightly more complicated version, with lots of joins that are 
-  # unnecessary if you're not breaking down by package class
-  $all_sql{tax} = "$select_all SUM(cust_bill_pkg_tax_location.amount)
-             $taxfrom
-             $where AND $istax
-             $group_all";
-
-  $all_sql{credit} = "$select_all SUM(cust_credit_bill_pkg.amount)
-                      $creditfrom
-                      $creditwhere AND $istax
-                      $group_all";
-}
-
-# "out of taxable region" sales
-$all_sql{out_sales} = 
-  "$select_all SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)
-  FROM (cust_bill_pkg $join_cust_pkg)
-  LEFT JOIN ($pkg_tax) AS pkg_tax USING (invnum, pkgnum)
-  LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt USING (billpkgnum)
-  $where AND $nottax
-  AND pkg_tax.taxnum IS NULL AND pkg_tax_exempt.taxnum IS NULL
-  $group_all"
-;
-
-$all_sql{out_sales} =~ s/EXEMPT_WHERE//;
-
 my %data;
 my %total;
+my %taxclass_name = { '' => '' };
+if ( $breakdown{taxclass} ) {
+  $taxclass_name{$_->taxclassnum} = $_->taxclass
+    foreach qsearch('tax_class');
+  $taxclass_name{''} = 'Unclassified';
+}
 foreach my $k (keys(%sql)) {
   my $stmt = $sql{$k};
   warn "\n".uc($k).":\n".$stmt."\n" if $DEBUG;
   my $sth = dbh->prepare($stmt);
-  # three columns: classnum, taxnum, value
+  # eight columns: pkgclass, taxclass, state, county, city, district
+  # taxnums (comma separated), value
+  # *sigh*
   $sth->execute 
     or die "failed to execute $k query: ".$sth->errstr;
   while ( my $row = $sth->fetchrow_arrayref ) {
-    $data{$k}{$row->[0]}{$row->[1]} = $row->[2];
+    my $bin = $data
+              {$row->[0]}
+              {$taxclass_name{$row->[1]}}
+              {$row->[2]}
+              {$row->[3] ? $row->[3] . ' County' : ''}
+              {$row->[4]}
+              {$row->[5]}
+            ||= [];
+    push @$bin, [ $k, $row->[6], $row->[7] ];
   }
 }
 warn "DATA:\n".Dumper(\%data) if $DEBUG > 1;
 
 foreach my $k (keys %all_sql) {
-  warn "\n".$all_sql{$k}."\n" if $DEBUG;
+  warn "\nTOTAL ".uc($k).":\n".$all_sql{$k}."\n" if $DEBUG;
   my $sth = dbh->prepare($all_sql{$k});
-  # two columns: classnum, value
+  # three columns: pkgclass, taxnums (comma separated), value
   $sth->execute 
     or die "failed to execute $k totals query: ".$sth->errstr;
   while ( my $row = $sth->fetchrow_arrayref ) {
-    $total{$k}{$row->[0]} = $row->[1];
-  }
-}
-warn "TOTALS:\n".Dumper(\%total);# if $DEBUG > 1;
-# so $data{tax}, for example, is now a hash with one entry
-# for each classnum, containing a hash with one entry for each
-# taxnum, containing the tax billed on that taxnum.
-# if with_pkgclass is off, then the classnum is always null.
-
-# integrity checks
-# unlinked tax collected
-my $out_tax_sql =
-  "SELECT SUM(cust_bill_pkg.setup)
-  FROM (cust_bill_pkg $join_cust)
-  LEFT JOIN cust_bill_pkg_tax_location USING (billpkgnum)
-  $where AND $istax AND cust_bill_pkg_tax_location.billpkgnum IS NULL"
-;
-my $unlinked_tax = FS::Record->scalar_sql($out_tax_sql);
-# unlinked tax credited
-my $out_credit_sql =
-  "SELECT SUM(cust_credit_bill_pkg.amount)
-  FROM cust_credit_bill_pkg
-  JOIN cust_bill_pkg USING (billpkgnum)
-  $join_cust
-  $where AND $istax AND cust_credit_bill_pkg.billpkgtaxlocationnum IS NULL"
-;
-my $unlinked_credit = FS::Record->scalar_sql($out_credit_sql);
-
-# all sales
-my $all_sales = FS::Record->scalar_sql(
-  "SELECT SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)
-  FROM cust_bill_pkg $join_cust $where AND $nottax"
-);
-
-#tax-report_groups filtering
-my($group_op, $group_value) = ( '', '' );
-if ( $cgi->param('report_group') =~ /^(=|!=) (.*)$/ ) {
-  ( $group_op, $group_value ) = ( $1, $2 );
-}
-my $group_test = sub { # to be applied to a tax label
-  my $label = shift;
-  return 1 unless $group_op; #in case we get called inadvertantly
-  if ( $label eq $out ) { #don't display "out of taxable region" in this case
-    0;
-  } elsif ( $group_op eq '=' ) {
-    $label =~ /^$group_value/;
-  } elsif ( $group_op eq '!=' ) {
-    $label !~ /^$group_value/;
-  } else {
-    die "guru meditation #00de: group_op $group_op\n";
+    my $bin = $total{$row->[0]} ||= [];
+    push @$bin, [ $k, $row->[1], $row->[2] ];
   }
-};
-
-my @pkgclasses;
-if ($with_pkgclass) {
-  @pkgclasses = qsearch('pkg_class', {});
-  push @pkgclasses, FS::pkg_class->new({
-    classnum  => '0',
-    classname => 'Unclassified',
-  });
-} else {
-  @pkgclasses = ( FS::pkg_class->new({
-    classnum  => '',
-    classname => '',
-  }) );
 }
-my %pkgclass_data;
-
-foreach my $class (@pkgclasses) {
-  my $classnum = $class->classnum;
-  my $classname = $class->classname;
-
-  # if show_taxclasses is on, %base_regions will contain the same data
-  # as %regions, but with taxclasses merged together (and ignoring report_group
-  # filtering).
-  my (%regions, %base_regions);
-
-  my @loc_params = qw(country state county);
-  push @loc_params, 'city' if $cgi->param('show_cities');
-  push @loc_params, 'district' if $cgi->param('show_districts');
-
-  foreach my $r ( qsearch({ 'table'     => 'cust_main_county', })) {
-    my $taxnum = $r->taxnum;
-    # set up a %regions entry for this region's tax label
-    my $label = $r->label(%label_opt);
-    next if $label eq $out;
-    $regions{$label} ||= { label => $label };
-
-    $regions{$label}->{$_} = $r->get($_) foreach @loc_params;
-    $regions{$label}->{taxnums} ||= [];
-    push @{ $regions{$label}->{taxnums} }, $r->taxnum;
-
-    my %x; # keys are data items (like 'tax', 'exempt_cust', etc.)
-    foreach my $k (keys %data) {
-      next unless exists($data{$k}{$classnum}{$taxnum});
-      $x{$k} = $data{$k}{$classnum}{$taxnum};
-      $regions{$label}{$k} += $x{$k};
-      if ( $k eq 'taxable' or $k =~ /^exempt/ ) {
-        $regions{$label}->{'sales'} += $x{$k};
-      }
-    }
-
-    my $owed = $data{'taxable'}{$classnum}{$taxnum} * ($r->tax/100);
-    $regions{$label}->{'owed'} += $owed;
-    $total{'owed'}{$classnum} += $owed;
-
-    if ( defined($regions{$label}->{'rate'})
-         && $regions{$label}->{'rate'} != $r->tax.'%' ) {
-      $regions{$label}->{'rate'} = 'variable';
-    } else {
-      $regions{$label}->{'rate'} = $r->tax.'%';
-    }
-
-    if ( $cgi->param('show_taxclasses') ) {
-      my $base_label = $r->label(%label_opt, 'with_taxclass' => 0);
-      $base_regions{$base_label} ||=
-      {
-        label   => $base_label,
-        tax     => 0,
-        credit  => 0,
-      };
-      $base_regions{$base_label}->{tax}    += $x{tax};
-      $base_regions{$base_label}->{credit} += $x{credit};
-    }
+warn "TOTALS:\n".Dumper(\%total) if $DEBUG > 1;
 
-  }
-
-  my @regions = map { $_->{label} }
-    sort {
-      ($b eq $out) <=> ($a eq $out)
-      or $a->{country} cmp $b->{country}
-      or $a->{state}   cmp $b->{state}
-      or $a->{county}  cmp $b->{county}
-      or $a->{city}    cmp $b->{city}
-    } 
-    grep { $_->{sales} > 0 or $_->{tax} > 0 or $_->{credit} > 0 }
-    values %regions;
-
-  #tax-report_groups filtering
-  @regions = grep &{$group_test}($_), @regions
-    if $group_op;
-
-  #calculate totals
-  my %taxclasses = ();
-  my %county = ();
-  my %state = ();
-  my %country = ();
-  foreach my $label (@regions) {
-    $taxclasses{$regions{$_}->{'taxclass'}} = 1
-      if $regions{$_}->{'taxclass'};
-    $county{$regions{$_}->{'county'}} = 1;
-    $state{$regions{$_}->{'state'}} = 1;
-    $country{$regions{$_}->{'country'}} = 1;
-  }
-
-  my $total_url_param = '';
-  my $total_url_param_invoiced = '';
-  if ( $group_op ) {
-
-    my @country = keys %country;
-    warn "WARNING: multiple countries on this grouped report; total links broken"
-      if scalar(@country) > 1;
-    my $country = $country[0];
-
-    my @state = keys %state;
-    warn "WARNING: multiple countries on this grouped report; total links broken"
-      if scalar(@state) > 1;
-    my $state = $state[0];
-
-    $total_url_param_invoiced =
-    $total_url_param =
-      'report_group='.uri_escape("$group_op $group_value").';'.
-      join(';', map 'taxclass='.uri_escape($_), keys %taxclasses );
-    $total_url_param .= ';'.
-      "country=$country;state=".uri_escape($state).';'.
-      join(';', map 'county='.uri_escape($_), keys %county ) ;
-
-  }
-
-  #ordering
-  @regions =
-    map $regions{$_},
-    sort { $a cmp $b }
-    @regions;
-
-  my @base_regions =
-    map $base_regions{$_},
-    sort { $a cmp $b }
-    keys %base_regions;
-
-  #add "Out of taxable" and total lines
-  if ( $total{out_sales}{$classnum} ) {
-    my %out = (
-      'sales' => $total{out_sales}{$classnum},
-      'label' => $out,
-      'rate' => ''
-    );
-    push @regions, \%out;
-    push @base_regions, \%out;
-  }
-
-  if ( @regions ) {
-    my %class_total = map { $_ => $total{$_}{$classnum} } keys(%total);
-    $class_total{is_total} = 1;
-    $class_total{sales} = sum(
-      @class_total{ 'taxable',
-                    'out_sales',
-                    grep(/^exempt/, keys %class_total) }
-    );
-
-    push @regions,      \%class_total;
-    push @base_regions, \%class_total;
-  }
-
-  $pkgclass_data{$classname} = {
-    classnum      => $classnum,
-    classname     => $classname,
-    regions       => \@regions,
-    base_regions  => \@base_regions,
-  };
-}
-
-if ( $with_pkgclass ) {
-  my $class_zero = delete( $pkgclass_data{'Unclassified'} );
-  @pkgclasses = map { $pkgclass_data{$_} }
-                sort { $a cmp $b }
-                keys %pkgclass_data;
-  push @pkgclasses, $class_zero;
-
-  my %grand_total = map {
-    $_ => sum( values(%{ $total{$_} }) )
-  } keys(%total);
-
-  $grand_total{sales} = $all_sales;
-
-  push @pkgclasses, {
-    classnum      => '',
-    classname     => 'Total',
-    regions       => [ \%grand_total ],
-    base_regions  => [ \%grand_total ],
-  }
-} else {
-  @pkgclasses = $pkgclass_data{''};
-}
-
-#-- 
+# $data{$pkgclass}{$taxclass}{$state}{$county}{$city}{$district} = [
+#   [ 'taxable',     taxnums, amount ],
+#   [ 'exempt_cust', taxnums, amount ],
+#   ...
+# ]
+# non-requested grouping levels simply collapse into key = ''
 
 my $money_char = $conf->config('money_char') || '$';
 my $money_sprintf = sub {
   $money_char. sprintf('%.2f', shift );
 };
-my $money_sprintf_nonzero = sub {
-  $_[0] == 0 ? '' : &$money_sprintf($_[0])
-};
 
 my $dateagentlink = "begin=$beginning;end=$ending";
 $dateagentlink .= ';agentnum='. $cgi->param('agentnum')
   if length($agentname);
-my $baselink   = $p. "search/cust_bill_pkg.cgi?$dateagentlink";
+my $saleslink  = $p. "search/cust_bill_pkg.cgi?$dateagentlink;nottax=1";
+my $taxlink    = $p. "search/cust_bill_pkg.cgi?$dateagentlink;istax=1";
 my $exemptlink = $p. "search/cust_tax_exempt_pkg.cgi?$dateagentlink";
-my $creditlink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;credit=1";
+my $creditlink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;credit=1;istax=1";
+
+my %taxrates;
+foreach my $tax (
+  qsearch('cust_main_county', {
+            country => $country,
+            tax => { op => '>', value => 0 }
+          }) )
+  {
+  $taxrates{$tax->taxnum} = $tax->tax;
+}
 
 </%init>
index 8a207aa..20aa07f 100755 (executable)
@@ -4,68 +4,34 @@
 
 <TABLE>
 
-% if ( $conf->config('tax-report_groups') ) {
-%   my @lines = $conf->config('tax-report_groups');
-    
-  <TR>
-    <TD ALIGN="right">Tax group</TD>
-    <TD>
-      <SELECT NAME="report_group">
-
-        <OPTION VALUE="">all</OPTION>
-
-%       foreach my $line ( @lines ) {
-%         $line =~ /^\s*(.+)\s+(=|!=)\s+(.*)\s*$/ #or next;
-%           or do { warn "bad report_group line: $line\n"; next; };
-%         my($label, $op, $value) = ($1, $2, $3);
-
-          <OPTION VALUE="<% "$op $value" %>"><% $label %></OPTION>
-%       }
-
-      </SELECT>
-    </TD>
-  </TR>
-
-% }
-
- <% include( '/elements/tr-select-agent.html', 'disable_empty'=>0 ) %>
-
- <% include( '/elements/tr-input-beginning_ending.html' ) %>
-
-%    if ( $city ) {
-   <TR>
-     <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_cities" VALUE="1" onclick="toggle_show_cities(this)"></TD>
-     <TD>Show cities</TD>
-   </TR>
-   <TR>
-     <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_districts" VALUE="1" DISABLED></TD>
-     <TD>Show districts</TD>
-   </TR>
-  <SCRIPT TYPE="text/javascript">
-  function toggle_show_cities() {
-    what = document.getElementsByName('show_cities')[0];
-    what.form.show_districts.disabled = !what.checked;
-    what.form.show_districts.checked  = what.checked;
-  }
-  toggle_show_cities();
-  </SCRIPT>
-% } 
-
-%    if ( $conf->exists('enable_taxclasses') ) {
-   <TR>
-     <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_taxclasses" VALUE="1"></TD>
-     <TD>Show tax classes</TD>
-   </TR>
-% } 
-
-% my @pkg_class = qsearch('pkg_class', {});
-% if ( @pkg_class ) {
-   <TR>
-     <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_pkgclasses" VALUE="1"></TD>
-     <TD>Show package classes</TD>
-   </TR>
-% } 
-
+  <& /elements/tr-select-agent.html, 'disable_empty'=>0 &>
+
+  <& /elements/tr-input-beginning_ending.html &>
+
+  <& /elements/tr-select.html,
+    'label'         => 'Country',
+    'field'         => 'country',
+    'options'       => \@countries,
+    'curr_value'    => ($conf->config('countrydefault') || 'US'),
+  &>
+
+  <& /elements/tr-select.html,
+    'label'         => 'For tax named ',
+    'field'         => 'taxname',
+    'options'       => \@taxnames,
+    'disable_empty' => 1,
+  &>
+
+  <& /elements/tr-checkbox-multiple.html,
+    'label'         => 'Break down by ',
+    'field'         => 'breakdown',
+    'options'       => \@breakdown,
+    'option_labels' => {
+      taxclass  => 'Tax class',
+      pkgclass  => 'Package class',
+      city      => 'City',
+    },
+  &>
 </TABLE>
 
 <BR><INPUT TYPE="submit" VALUE="Get Report">
@@ -80,12 +46,23 @@ die "access denied"
 
 my $conf = new FS::Conf;
 
-my $city_sql = "SELECT COUNT(*) FROM cust_main_county
-                  WHERE city != '' AND city IS NOT NULL
-                  LIMIT 1";
-
-my $city_sth = dbh->prepare($city_sql) or die dbh->errstr;
-$city_sth->execute or die $city_sth->errstr;
-my $city = $city_sth->fetchrow_arrayref->[0];
+my $sth = dbh->prepare('SELECT DISTINCT(COALESCE(taxname, \'Tax\')) FROM cust_main_county');
+$sth->execute or die $sth->errstr;
+my @taxnames = map { $_->[0] } @{ $sth->fetchall_arrayref };
+
+$sth = dbh->prepare('SELECT DISTINCT(country) FROM cust_location');
+$sth->execute or die $sth->errstr;
+my @countries = map { $_->[0] } @{ $sth->fetchall_arrayref };
+
+my @breakdown;
+if ( $conf->exists('enable_taxclasses') ) {
+  push @breakdown, 'taxclass';
+}
+if ( FS::pkg_class->count() > 0 ) {
+  push @breakdown, 'pkgclass';
+}
+if ( FS::cust_main_county->count("city is not null and city != ''") > 0 ) {
+  push @breakdown, 'city';
+}
 
 </%init>
index b4d40ae..e74f379 100644 (file)
@@ -36,6 +36,8 @@ my $count_query = "SELECT COUNT(*) FROM sales";
 my $salesnum;
 if ( $cgi->param('salesnum') =~ /^(\d+)$/ ) {
   $salesnum = $1;
+} else {
+  $cgi->delete('salesnum');
 }
 
 my $title = 'Sales person commission';
@@ -43,13 +45,14 @@ $title .= ': '. time2str($date_format, $beginning). ' to '.
                 time2str($date_format, $ending)
   if $beginning;
 
+my $paid = $cgi->param('paid') ? 1 : 0;
+$title .= ' - paid sales only' if $paid;
+
 my $cust_main_sales = $cgi->param('cust_main_sales') eq 'Y' ? 'Y' : '';
 
 my $sales_link = [ 'sales_pkg_class.html?'.
-                     "begin=$beginning;".
-                     "end=$ending;".
-                     "cust_main_sales=$cust_main_sales;".
-                     "salesnum=",
+                   # pass all of our parameters along
+                   $cgi->query_string. ';salesnum=',
                    'salesnum'
                  ];
 
@@ -64,6 +67,7 @@ my $sales_sub_maker = sub {
       $beginning,
       $ending,
       'cust_main_sales' => $cust_main_sales,
+      'paid' => $paid,
     );
     $total += $_->get($field) foreach @cust_bill_pkg;
 
index da5d512..8bb6bde 100644 (file)
@@ -9,7 +9,7 @@
                           $sales_sub_maker->('setup'),
                           $sales_sub_maker->('recur'),
                           $commission_sub, ],
-     'links'         => [ '', '', '', $commission_link ],
+     'links'         => [ '', $sales_link, $sales_link, $commission_link ],
      'align'         => 'lrrr',
      'query'         => { 'table'   => 'sales_pkg_class',
                           'hashref' => { 'salesnum' => $salesnum },
@@ -40,6 +40,9 @@ $title .= ': '. time2str($date_format, $beginning). ' to '.
   if $beginning;
 
 my $cust_main_sales = $cgi->param('cust_main_sales') eq 'Y' ? 'Y' : '';
+my $paid = $cgi->param('paid') ? 1 : 0;
+
+$title .= " - paid sales only" if $paid;
 
 my $sales_link = [ 'cust_bill_pkg.cgi?'.
                      "begin=$beginning;".
@@ -55,18 +58,17 @@ my $sales_sub_maker = sub {
   my $field = shift;
   sub {
     my $sales_pkg_class = shift;
-
-    #efficiency improvement: ask the db for a sum instead of all the records
-    my $total = 0;
-    my @cust_bill_pkg = $sales->cust_bill_pkg(
+    # could be even more efficient but this is pretty good
+    my $search = $sales->cust_bill_pkg_search(
       $beginning,
       $ending,
       'cust_main_sales' => $cust_main_sales,
       'classnum'        => $sales_pkg_class->classnum,
+      'paid'            => $paid,
     );
-    $total += $_->get($field) foreach @cust_bill_pkg;
-
-    $money_char. sprintf('%.2f', $total);
+    $search->{'select'} = "SUM(cust_bill_pkg.$field) AS total";
+    my $result = qsearchs($search);
+    $money_char. sprintf('%.2f', $result ? $result->get('total') : 0);
   };
 };
 
@@ -85,6 +87,15 @@ my $commission_sub = sub {
   $money_char. sprintf('%.2f', $total_credit);
 };
 
+my $sales_link = [ 'cust_bill_pkg.cgi?'.
+                    "begin=$beginning;".
+                    "end=$ending;".
+                    "cust_main_sales=$cust_main_sales;".
+                    "salesnum=$salesnum;".
+                    "classnum=",
+                   'classnum'
+                 ];
+
 my $commission_link = [ 'cust_credit.html?'.
                           "begin=$beginning;".
                           "end=$ending;".
index e286305..e0dd7b9 100644 (file)
 % if ( $conf->exists('enable_taxproducts') ) {
 <TR>
   <TD ALIGN="right"><% mt('Tax location') |h %></TD>
-  <TD BGCOLOR="#ffffff"><% $cust_main->geocode('cch') %></TD>
+% my $tax_location = $conf->exists('tax-ship_address')
+%                    ? $cust_main->ship_location
+%                    : $cust_main->bill_location;
+  <TD BGCOLOR="#ffffff"><% $tax_location->geocode('cch') %></TD>
 </TR>
 % }
 <TR>
index 63a050c..a851d99 100644 (file)
@@ -22,7 +22,7 @@
 %                   })
 %           or next;
           <TD ALIGN="right">&nbsp;&nbsp;&nbsp;<% $phone_type->typename %> phone</TD>
-          <TD BGCOLOR="#FFFFFF"><% $contact_phone->phonenum |h %></TD>
+          <TD BGCOLOR="#FFFFFF"><% $contact_phone->phonenum_pretty |h %></TD>
 %       }
 
       </TR>
index 077dc77..833e862 100644 (file)
@@ -23,6 +23,7 @@
 
 
 </TABLE></TD></TR></TABLE>
+<BR>
 
 <& /elements/table-tickets.html, object => $cust_svc &>