From 511df0f6b2b42849d3174fa6c8267a9e36191baf Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 16 Oct 2013 17:49:20 -0700 Subject: sales commission report improvements, #25256 --- FS/FS/sales.pm | 81 ++++++++++++++++++++------ httemplate/search/cust_bill_pkg.cgi | 41 +++++++------ httemplate/search/report_sales_commission.html | 16 +++-- httemplate/search/sales_commission.html | 12 ++-- httemplate/search/sales_pkg_class.html | 27 ++++++--- 5 files changed, 124 insertions(+), 53 deletions(-) diff --git a/FS/FS/sales.pm b/FS/FS/sales.pm index 82c875a36..bdeaf1b68 100644 --- a/FS/FS/sales.pm +++ b/FS/FS/sales.pm @@ -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) for which this +sales person could receive commission. + +START and END are an optional date range to limit the results. + +OPTIONS may contain: +- I: 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: limit to this package classnum. +- I: 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 { diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index 4c5e90f6a..fc74b542e 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -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"; @@ -704,6 +687,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"; diff --git a/httemplate/search/report_sales_commission.html b/httemplate/search/report_sales_commission.html index cc17e6bed..792c3353e 100644 --- a/httemplate/search/report_sales_commission.html +++ b/httemplate/search/report_sales_commission.html @@ -20,14 +20,22 @@ -<& /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 &> diff --git a/httemplate/search/sales_commission.html b/httemplate/search/sales_commission.html index b4d40aeb2..e74f3792e 100644 --- a/httemplate/search/sales_commission.html +++ b/httemplate/search/sales_commission.html @@ -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; diff --git a/httemplate/search/sales_pkg_class.html b/httemplate/search/sales_pkg_class.html index da5d512d5..8bb6bde4c 100644 --- a/httemplate/search/sales_pkg_class.html +++ b/httemplate/search/sales_pkg_class.html @@ -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;". -- cgit v1.2.1 From d32ed470ba0869a4005c0c577b98246c48f8be7d Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 17 Oct 2013 14:33:46 -0700 Subject: daily sales/receipts report: fix customer class and date selection, #24853 --- FS/FS/Report/Table/Daily.pm | 4 ++++ httemplate/elements/tr-input-beginning_ending.html | 4 ++-- httemplate/graph/report_money_time_daily.html | 9 ++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/FS/FS/Report/Table/Daily.pm b/FS/FS/Report/Table/Daily.pm index 6087b0dcc..570fefe4d 100644 --- a/FS/FS/Report/Table/Daily.pm +++ b/FS/FS/Report/Table/Daily.pm @@ -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; } diff --git a/httemplate/elements/tr-input-beginning_ending.html b/httemplate/elements/tr-input-beginning_ending.html index ffc903875..91f55eb69 100644 --- a/httemplate/elements/tr-input-beginning_ending.html +++ b/httemplate/elements/tr-input-beginning_ending.html @@ -7,7 +7,7 @@ From date: - MAXLENGTH=<%$maxlength%>>
m/d/y<% $time_hint %> + MAXLENGTH=<%$maxlength%>>
m/d/y<% $time_hint %> - <% 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', - ) - %> + &> % } @@ -189,24 +178,33 @@ - <% 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 &> + + <& /elements/tr-title.html, value => mt('Location search options') &> + +% my @location_options = qw(cust nocust census nocensus); + <& /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", + }, + 'value' => { map { $_ => 1 } @location_options }, + &> - -   - + <& /elements/tr-title.html, value => mt('Display options') &> - - Display options - - <% include( '/elements/tr-select-cust-fields.html' ) %> + <& /elements/tr-select-cust-fields.html &> -- cgit v1.2.1 From ad2152b8bfd85b904e363dfded50c810a96daff0 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 19 Oct 2013 14:42:03 -0700 Subject: preserve query strings when internal-redirecting through login page --- httemplate/loginout/login.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httemplate/loginout/login.html b/httemplate/loginout/login.html index d06d0a8fc..3c6e2ae9f 100644 --- a/httemplate/loginout/login.html +++ b/httemplate/loginout/login.html @@ -14,7 +14,7 @@ %#
- + -- cgit v1.2.1 From 0af30a9f44dc538f8696e20d02e32183b8ccf82b Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 19 Oct 2013 15:50:23 -0700 Subject: prorate to calendar days, #22550 --- FS/FS/part_pkg/prorate_calendar.pm | 223 +++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 FS/FS/part_pkg/prorate_calendar.pm diff --git a/FS/FS/part_pkg/prorate_calendar.pm b/FS/FS/part_pkg/prorate_calendar.pm new file mode 100644 index 000000000..83a80f5d0 --- /dev/null +++ b/FS/FS/part_pkg/prorate_calendar.pm @@ -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; -- cgit v1.2.1 From 6bcf9060e47a38b1e209b2be09f70dcce4b0e8c0 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sat, 19 Oct 2013 22:31:50 -0700 Subject: 2013 census data available --- FS/FS/Misc/Geo.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FS/FS/Misc/Geo.pm b/FS/FS/Misc/Geo.pm index b5cc325d1..d9dcf3fd1 100644 --- a/FS/FS/Misc/Geo.pm +++ b/FS/FS/Misc/Geo.pm @@ -82,7 +82,7 @@ sub get_censustract_ffiec { my($zip5, $zip4) = split('-',$location->{zip}); - $year ||= '2012'; + $year ||= '2013'; my @ffiec_args = ( __VIEWSTATE => $viewstate, __EVENTVALIDATION => $eventvalidation, -- cgit v1.2.1 From 2029d3997a6c795a55bb869298b08958fab4ee56 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sat, 19 Oct 2013 22:54:51 -0700 Subject: add -c flag to skip cdr and h_cdr updates, RT#21464 --- FS/bin/freeside-upgrade | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/FS/bin/freeside-upgrade b/FS/bin/freeside-upgrade index 06ec96205..893f5d41d 100755 --- a/FS/bin/freeside-upgrade +++ b/FS/bin/freeside-upgrade @@ -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,12 @@ unless ( driver_name =~ /^mysql/i ) { @statements; } +if ( $opt_c ) { + @statements = + grep { $_ !~ /^ *ALTER +TABLE +(h_)?cdr /i } + @statements; +} + if ( $DRY_RUN ) { print join(";\n", @statements ). ";\n"; @@ -312,7 +318,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 +343,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. -- cgit v1.2.1 From d25be172b2bfdc1a49e3bf54719155cb853391bb Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sat, 19 Oct 2013 22:55:16 -0700 Subject: add -c flag to skip cdr and h_cdr updates, RT#21464 --- FS/bin/freeside-upgrade | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/FS/bin/freeside-upgrade b/FS/bin/freeside-upgrade index 893f5d41d..6fbf39bbc 100755 --- a/FS/bin/freeside-upgrade +++ b/FS/bin/freeside-upgrade @@ -155,9 +155,15 @@ unless ( driver_name =~ /^mysql/i ) { } if ( $opt_c ) { + @statements = grep { $_ !~ /^ *ALTER +TABLE +(h_)?cdr /i } @statements; + + @statements = + grep { $_ !~ /^ *CREATE +INDEX +(h_)?cdr\d+ /i } + @statements; + } if ( $DRY_RUN ) { -- cgit v1.2.1 From e69b549276439ab52dc6eadcbeaf3b5409926edb Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sat, 19 Oct 2013 23:24:20 -0700 Subject: ignore setup+start date on upgrade, RT#25516, RT#21464 --- FS/FS/Upgrade.pm | 3 +++ FS/FS/cust_pkg.pm | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index 037c4b3f2..a7fe99f90 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -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"; diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 009c81e06..face10a5a 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -4,7 +4,7 @@ use base qw( FS::otaker_Mixin FS::cust_main_Mixin FS::Sales_Mixin FS::m2m_Common FS::option_Common ); use strict; -use vars qw($disable_agentcheck $DEBUG $me); +use vars qw( $disable_agentcheck $DEBUG $me $upgrade ); use Carp qw(cluck); use Scalar::Util qw( blessed ); use List::Util qw(min max); @@ -54,6 +54,8 @@ $me = '[FS::cust_pkg]'; $disable_agentcheck = 0; +$upgrade = 0; #go away after setup+start dates cleaned up for old customers + sub _cache { my $self = shift; my ( $hashref, $cache ) = @_; @@ -656,7 +658,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; -- cgit v1.2.1 From 83118399ef2b240bc68650c3ddf2c075d4ffd568 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sun, 20 Oct 2013 20:15:16 -0700 Subject: update census tract coding for changes in FFIEC site, RT#25258 --- FS/FS/Conf.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index dd4cd6869..0dd3212fb 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -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 ) ], }, { -- cgit v1.2.1 From e7f2082c627a349e4556d00023f4dae317b2591d Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sun, 20 Oct 2013 20:30:11 -0700 Subject: fix MANIFEST, eliminate warning about missing MANIFEST.SKIP --- FS/MANIFEST | 1 - 1 file changed, 1 deletion(-) diff --git a/FS/MANIFEST b/FS/MANIFEST index 7a460dac3..e0969f09c 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -1,6 +1,5 @@ Changes MANIFEST -MANIFEST.SKIP Makefile.PL bin/freeside-addoutsource bin/freeside-addoutsourceuser -- cgit v1.2.1 From 275ce0155bd34829ee97b3f0ca48228ca3c5daca Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sun, 20 Oct 2013 20:42:45 -0700 Subject: strip non-digits from contact_phone.phonenum, RT#24052 --- FS/FS/contact_phone.pm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/FS/FS/contact_phone.pm b/FS/FS/contact_phone.pm index ad8e8f737..55cb09dc3 100644 --- a/FS/FS/contact_phone.pm +++ b/FS/FS/contact_phone.pm @@ -124,6 +124,10 @@ 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; } @@ -131,8 +135,6 @@ sub check { =head1 BUGS -The author forgot to customize this manpage. - =head1 SEE ALSO L, schema.html from the base documentation. -- cgit v1.2.1 From 24f8d52f90be8e99b10734f49d451e1e70afdfbc Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sun, 20 Oct 2013 20:59:30 -0700 Subject: normalize contact phone formatting --- FS/FS/contact_phone.pm | 23 +++++++++++++++++++++++ httemplate/elements/contact.html | 2 +- httemplate/view/cust_main/contacts_new.html | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/FS/FS/contact_phone.pm b/FS/FS/contact_phone.pm index 55cb09dc3..7ba85234a 100644 --- a/FS/FS/contact_phone.pm +++ b/FS/FS/contact_phone.pm @@ -131,6 +131,29 @@ sub check { $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; + } + +} + =back =head1 BUGS diff --git a/httemplate/elements/contact.html b/httemplate/elements/contact.html index 3d5177612..8abce058e 100644 --- a/httemplate/elements/contact.html +++ b/httemplate/elements/contact.html @@ -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" diff --git a/httemplate/view/cust_main/contacts_new.html b/httemplate/view/cust_main/contacts_new.html index 63a050c04..a851d99ed 100644 --- a/httemplate/view/cust_main/contacts_new.html +++ b/httemplate/view/cust_main/contacts_new.html @@ -22,7 +22,7 @@ % }) % or next; - + % } -- cgit v1.2.1 From cc58d576f8c2da6d01751ca3234b6a1ec11db358 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Tue, 22 Oct 2013 14:38:25 -0700 Subject: fix default report dates, fallout from #24853 --- httemplate/elements/tr-input-beginning_ending.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httemplate/elements/tr-input-beginning_ending.html b/httemplate/elements/tr-input-beginning_ending.html index 91f55eb69..be6e03c6f 100644 --- a/httemplate/elements/tr-input-beginning_ending.html +++ b/httemplate/elements/tr-input-beginning_ending.html @@ -7,7 +7,7 @@ - + + <%shared> my $init = 0; @@ -21,6 +53,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'); <%doc> Set up a text input field with input masking. diff --git a/httemplate/pref/pref-process.html b/httemplate/pref/pref-process.html index 962ee51b6..7edf8921c 100644 --- a/httemplate/pref/pref-process.html +++ b/httemplate/pref/pref-process.html @@ -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 diff --git a/httemplate/pref/pref.html b/httemplate/pref/pref.html index d2b8835e7..ccfeecd77 100644 --- a/httemplate/pref/pref.html +++ b/httemplate/pref/pref.html @@ -137,6 +137,13 @@ Interface + + + + +
   <% $phone_type->typename %> phone<% $contact_phone->phonenum |h %><% $contact_phone->phonenum_pretty |h %>
From date: MAXLENGTH=<%$maxlength%>>
m/d/y<% $time_hint %>
MAXLENGTH=<%$maxlength%>>
m/d/y<% $time_hint %>
Don't copy MAC address delimiters to clipboard + option('enable_mask_clipboard_hack') ? 'CHECKED' : '' %>> +

-- cgit v1.2.1 From 422e87b24b866eaa420258d97d4ccdc089019f88 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 24 Oct 2013 16:04:10 -0700 Subject: masked input clipboard hack, part 2 --- httemplate/elements/tr-input-mask.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/httemplate/elements/tr-input-mask.html b/httemplate/elements/tr-input-mask.html index 8a494c8de..922f22f4b 100644 --- a/httemplate/elements/tr-input-mask.html +++ b/httemplate/elements/tr-input-mask.html @@ -37,7 +37,10 @@ t.onkeydown = function(e) { }; container.onkeyup = function(e) { if ( container.previous ) { - setTimeout(function() {container.previous.focus();}, 10); + setTimeout(function() { + container.previous.value = container.value; + container.previous.focus(); + }, 10); } return true; } -- cgit v1.2.1 From 7427b3722522f049c9ebdb4ba3780a91b88c414f Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 24 Oct 2013 17:26:00 -0700 Subject: disable old no_censustract and with_geocode search options and move to cust_pkg search, #23582 --- FS/FS/cust_main/Search.pm | 31 +------------------------------ FS/FS/cust_pkg.pm | 20 ++++++++++++++++---- httemplate/search/report_cust_main.html | 14 -------------- httemplate/search/report_cust_pkg.html | 6 ++++++ 4 files changed, 23 insertions(+), 48 deletions(-) diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index 182527ff9..70d12c97d 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -667,22 +667,6 @@ sub search { $pkgwhere .= "AND (cancel = 0 or cancel is null)" 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 +934,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 +947,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; } diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index face10a5a..771d3ec75 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -4287,21 +4287,29 @@ For FCC 477 reporting, mostly. =item location_cust -Limit to packages whose service location is the same as the customer's +Limit to packages whose service locations are the same as the customer's default service location. =item location_nocust -Limit to packages whose service location is not the customer's default +Limit to packages whose service locations are not the customer's default service location. =item location_census -Limit to packages whose service location has a census tract. +Limit to packages whose service locations have census tracts. =item location_nocensus -Limit to packages whose service location doesn't have a census tract. +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 @@ -4545,6 +4553,10 @@ sub search { 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 diff --git a/httemplate/search/report_cust_main.html b/httemplate/search/report_cust_main.html index ebff7fadb..1ceb48e1e 100755 --- a/httemplate/search/report_cust_main.html +++ b/httemplate/search/report_cust_main.html @@ -171,20 +171,6 @@ field => 'current_balance', &> - - <% mt('Without census tract') |h %> - - - -% if ( $conf->exists('enable_taxproducts') ) { - - - <% mt('With hardcoded tax location') |h %> - - - -% } - <% mt('With email address(es)') |h %> diff --git a/httemplate/search/report_cust_pkg.html b/httemplate/search/report_cust_pkg.html index b3f2004b8..e75a0985b 100755 --- a/httemplate/search/report_cust_pkg.html +++ b/httemplate/search/report_cust_pkg.html @@ -190,6 +190,9 @@ <& /elements/tr-title.html, value => mt('Location search options') &> % 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', @@ -198,6 +201,8 @@ '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 }, &> @@ -274,4 +279,5 @@ my %checkbox = ( 'cancel' => 1, ); +my $conf = FS::Conf->new; -- cgit v1.2.1 From 50c2e50db2999c61db1c1c72ae416482ef370d99 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 24 Oct 2013 20:10:28 -0700 Subject: selectable choice for arbitrary fields, RT#25623 --- httemplate/edit/elements/svc_Common.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/httemplate/edit/elements/svc_Common.html b/httemplate/edit/elements/svc_Common.html index 321c68545..64484eba0 100644 --- a/httemplate/edit/elements/svc_Common.html +++ b/httemplate/edit/elements/svc_Common.html @@ -121,6 +121,11 @@ $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' -- cgit v1.2.1 From 96a3ff4adba5871dae5b6f0ea657cc38fbe83513 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 24 Oct 2013 20:11:50 -0700 Subject: style --- httemplate/edit/elements/svc_Common.html | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/httemplate/edit/elements/svc_Common.html b/httemplate/edit/elements/svc_Common.html index 64484eba0..413150858 100644 --- a/httemplate/edit/elements/svc_Common.html +++ b/httemplate/edit/elements/svc_Common.html @@ -101,14 +101,17 @@ 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 ' . @@ -116,11 +119,13 @@ ')'; $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*/, -- cgit v1.2.1 From ca00186e5e6b7eee73140546cde55758f81ed4de Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 24 Oct 2013 20:12:50 -0700 Subject: spacing --- httemplate/view/svc_external.cgi | 1 + 1 file changed, 1 insertion(+) diff --git a/httemplate/view/svc_external.cgi b/httemplate/view/svc_external.cgi index 077dc77b7..833e86247 100644 --- a/httemplate/view/svc_external.cgi +++ b/httemplate/view/svc_external.cgi @@ -23,6 +23,7 @@ +
<& /elements/table-tickets.html, object => $cust_svc &> -- cgit v1.2.1 From a6d62e3bc88e63d71fc972303210aec5464b2219 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 24 Oct 2013 21:58:40 -0700 Subject: fix next-bill-ignore-time, RT#24318, RT#24476, RT#12570 --- FS/FS/cust_main/Billing.pm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index 0a364f509..b8a71d4a9 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -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, -- cgit v1.2.1 From 7b20719d5f7269adfebf55975766cd6eca4a7dd5 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 24 Oct 2013 23:25:46 -0700 Subject: prevent browsers from saving passwords --- httemplate/elements/input-text.html | 1 + httemplate/elements/tr-password.html | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/httemplate/elements/input-text.html b/httemplate/elements/input-text.html index 827941501..18272ebe2 100644 --- a/httemplate/elements/input-text.html +++ b/httemplate/elements/input-text.html @@ -5,6 +5,7 @@ <% $size %> <% $maxlength %> <% $style %> + <% $opt{autocomplete} ? 'autocomplete="off"' : '' %> <% $opt{disabled} %> <% $onchange %> ><% $opt{'postfix'} %> diff --git a/httemplate/elements/tr-password.html b/httemplate/elements/tr-password.html index d19c4ed0e..8ad303c67 100644 --- a/httemplate/elements/tr-password.html +++ b/httemplate/elements/tr-password.html @@ -1,3 +1,4 @@ <& tr-input-text.html, @_, - 'type' => 'password', + 'type' => 'password', + 'autocomplete' => 1, &> -- cgit v1.2.1 From 2e7ce01632012ccc0dd440a8bc37a9ec9bd55fac Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 24 Oct 2013 23:29:58 -0700 Subject: limit sales reports for employee, RT#25524 --- FS/FS/Schema.pm | 1 + FS/FS/access_user.pm | 14 +++++++++ httemplate/edit/access_user.html | 22 ++++++++------ httemplate/search/report_sales_commission.html | 42 ++++++++++++++++---------- 4 files changed, 53 insertions(+), 26 deletions(-) diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index fcc2092a7..59c2abee9 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -3804,6 +3804,7 @@ 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', diff --git a/FS/FS/access_user.pm b/FS/FS/access_user.pm index 7c25acbe3..c938474a6 100644 --- a/FS/FS/access_user.pm +++ b/FS/FS/access_user.pm @@ -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), 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 diff --git a/httemplate/edit/access_user.html b/httemplate/edit/access_user.html index b087943c2..2d39f969c 100644 --- a/httemplate/edit/access_user.html +++ b/httemplate/edit/access_user.html @@ -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 = <set('_password', ''); + my ($cgi, $access_user, $fields_listref, $opt_hashref) = @_; + $access_user->_password(''); } sub field_callback { diff --git a/httemplate/search/report_sales_commission.html b/httemplate/search/report_sales_commission.html index 792c3353e..19af428d8 100644 --- a/httemplate/search/report_sales_commission.html +++ b/httemplate/search/report_sales_commission.html @@ -4,25 +4,34 @@ -<& /elements/tr-select-agent.html, - 'onchange' => 'agent_changed(this)', -&> +% if ( $curuser->report_salesnum ) { - + + + <& /elements/tr-select-sales.html, + 'empty_label' => 'all', + &> + +% } <& /elements/tr-checkbox.html, 'label' => 'Customer sales person if there is no package sales person', @@ -46,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'); -- cgit v1.2.1 From ac01b85bf09d058d3cda097dffbf4e8fce5f4305 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 31 Oct 2013 16:42:18 -0700 Subject: improved tax report, #23349 --- httemplate/search/cust_bill_pkg.cgi | 83 ++- httemplate/search/cust_tax_exempt_pkg.cgi | 39 +- httemplate/search/report_tax.cgi | 847 ++++++++++-------------------- httemplate/search/report_tax.html | 115 ++-- 4 files changed, 357 insertions(+), 727 deletions(-) diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index fc74b542e..d86641de3 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -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; @@ -333,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 ). ' )' @@ -373,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) @@ -405,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 @@ -440,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 ) { @@ -472,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)"; } @@ -555,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 @@ -572,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 diff --git a/httemplate/search/cust_tax_exempt_pkg.cgi b/httemplate/search/cust_tax_exempt_pkg.cgi index 40b9ed78f..382dbc476 100644 --- a/httemplate/search/cust_tax_exempt_pkg.cgi +++ b/httemplate/search/cust_tax_exempt_pkg.cgi @@ -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) : ''; diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi index d71fcf90b..111f22d3d 100755 --- a/httemplate/search/report_tax.cgi +++ b/httemplate/search/report_tax.cgi @@ -1,283 +1,184 @@ -<% 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 +&> -<% include('/elements/table-grid.html') %> +<& /elements/table-grid.html &> - - - - - - -% unless ( $cgi->param('show_taxclasses') ) { - - - - - -% } + + + + + + + + + + + - - - - - - - - - + + + + + - - - - + + + + -% foreach my $class (@pkgclasses ) { -% next if @{ $class->{regions} } == 0; -% if ( $class->{classname} ) { - - +% 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 + +% # Row label + +% # 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); + +% # exemptions +% foreach(qw(cust pkg)) { + +% } + +% # taxable + +% # 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'); + + +% # estimated tax + + +% # invoiced tax + +% # credited tax + + +% # net tax due + + -% } -% 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 = ''; -% my $bme = ''; - -% if ( $region->{'is_total'} ) { - - -% } else { - - <<%$td%>><% $region->{'label'} %> +% }; + +% 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' }); + +% if ( $breakdown{pkgclass} ) { + + + % } - <<%$td%> ALIGN="right"> - <% &$money_sprintf( $region->{'sales'} ) %> - -% if ( $region->{'label'} eq $out ) { - <<%$td%> COLSPAN=12> -% } else { #not $out - <<%$td%>> - - <<%$td%> ALIGN="right"> - <% &$money_sprintf( $region->{'exempt_cust'} ) %> - - <<%$td%>> - - <<%$td%> ALIGN="right"> - <% &$money_sprintf( $region->{'exempt_pkg'} ) %> - - <<%$td%>> - - <<%$td%> ALIGN="right"> - <% &$money_sprintf( $region->{'exempt_monthly'} ) %> - - <<%$td%>> = - <<%$td%> ALIGN="right"> - <% &$money_sprintf( $region->{'taxable'} ) %> - - <<%$td%>><% $region->{'label'} eq 'Total' ? '' : "$bigmath X $bme" %> - <<%$td%> ALIGN="right"><% $region->{'rate'} %> - <<%$td%>><% $region->{'label'} eq 'Total' ? '' : "$bigmath = $bme" %> - <<%$tdh%> ALIGN="right"> - <% &$money_sprintf( $region->{'owed'} ) %> - -% } # 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"> - <% &$money_sprintf_nonzero( $region->{'tax'} ) %> - - <<%$td%>> - <<%$td%> ALIGN="right"> - <% &$money_sprintf_nonzero( $region->{'credit'} ) %> - - <<%$td%> COLSPAN=2> -% } else { #not $out - <<%$tdh%> ALIGN="right"> - <% &$money_sprintf( $region->{'tax'} ) %> - - <<%$tdh%>> - - <<%$tdh%> ALIGN="right"> - <% &$money_sprintf( $region->{'credit'} ) %> - - <<%$tdh%>> = - <<%$tdh%> ALIGN="right"> - <% &$money_sprintf( $region->{'tax'} - $region->{'credit'} ) %> - -% } -% } # show_taxclasses - - -% } # foreach $region - -%} # foreach $class - +% $row = 0; +% $classlink = "classnum=".($pkgclass || 0) if $breakdown{pkgclass}; +% &{ $descend }( $data{$pkgclass}, '' ); +% # and now totals + + +% &{ $descend }( $total{$pkgclass}, 'Total' ); + +% } # foreach $pkgclass
Download full results
as Excel spreadsheet
SalesRateTax owedTax invoicedTax creditedTax collectedSalesRateEstimated taxTax invoicedTax creditedNet tax due
TotalNon-taxableNon-taxableNon-taxableTaxableTotalNon-taxableNon-taxableNon-taxableTaxable
(tax-exempt customer)(tax-exempt package)(monthly exemption)
(tax-exempt customer)(tax-exempt package)(monthly exemption)
<% $class->{classname} %>
<% $label |h %> + "> + <% $sales %> + + + "> + <% $money_sprintf->($values{"exempt_$_"}) %> + + + "> + <% $money_sprintf->($values{"exempt_monthly"}) %> + + + "> + <% $money_sprintf->($values{taxable}) %> + + × <% $rate %> = <% $rate eq 'variable' + ? '' + : $money_sprintf->( $values{taxable} * $rate / 100 ) %> + + "> + <% $money_sprintf->( $values{tax} ) %> + + + "> + <% $money_sprintf->( $values{credited} ) %> + + = <% $money_sprintf->( $values{tax} - $values{credited} ) %>
Total
<% $class->classname %>
-% if ( $cgi->param('show_taxclasses') ) { - -
- <% include('/elements/table-grid.html') %> - - - Tax invoiced - - Tax credited - - Tax collected - - -% #some false laziness w/above -% foreach my $class (@pkgclasses) { -% if ( $class->{classname} ) { - - <% $class->{classname} %> - -% } - -% 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; - - - <<%$td%>><% $region->{'label'} %> -% if ( $region->{'label'} eq $out ) { - <<%$td%> ALIGN="right"> - <% &$money_sprintf_nonzero( $region->{'tax'} ) %> - - <<%$td%>> - <<%$td%> ALIGN="right"> - <% &$money_sprintf_nonzero( $region->{'credit'} ) %> - - <<%$td%> COLSPAN=2> -% } else { #not $out - <<%$td%> ALIGN="right"> - <% &$money_sprintf( $region->{'tax'} ) %> - - <<%$td%>> - - <<%$tdh%> ALIGN="right"> - <% &$money_sprintf( $region->{'credit'} ) %> - - <<%$td%>> = - <<%$tdh%> ALIGN="right"> - <% &$money_sprintf( $region->{'tax'} - $region->{'credit'} ) %> - - -% } # if $out -% } #foreach $region -% } #foreach $class - - - -% } # 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; +} diff --git a/httemplate/search/report_tax.html b/httemplate/search/report_tax.html index 8a207aafb..20aa07fc8 100755 --- a/httemplate/search/report_tax.html +++ b/httemplate/search/report_tax.html @@ -4,68 +4,34 @@ -% if ( $conf->config('tax-report_groups') ) { -% my @lines = $conf->config('tax-report_groups'); - - - - - - -% } - - <% include( '/elements/tr-select-agent.html', 'disable_empty'=>0 ) %> - - <% include( '/elements/tr-input-beginning_ending.html' ) %> - -% if ( $city ) { - - - - - - - - - -% } - -% if ( $conf->exists('enable_taxclasses') ) { - - - - -% } - -% my @pkg_class = qsearch('pkg_class', {}); -% if ( @pkg_class ) { - - - - -% } - + <& /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', + }, + &>
Tax group - -
Show cities
Show districts
Show tax classes
Show package classes

@@ -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'; +} -- cgit v1.2.1 From 999ee63106a78d26f4375b9a71d6de78a03a4116 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sat, 2 Nov 2013 12:25:18 -0700 Subject: doc: stop referring to schema.html --- eg/table_template.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eg/table_template.pm b/eg/table_template.pm index 9c71b3adc..0a6f85156 100644 --- a/eg/table_template.pm +++ b/eg/table_template.pm @@ -108,7 +108,7 @@ The author forgot to customize this manpage. =head1 SEE ALSO -L, schema.html from the base documentation. +L =cut -- cgit v1.2.1 From caad814a67620dad4aa97f0c5be8adb324956cc1 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sat, 2 Nov 2013 13:47:50 -0700 Subject: contact search, RT#25687 (also possibly #25583 and #22991) --- FS/FS/contact.pm | 65 +++++++++++++++- FS/FS/contact_email.pm | 55 +++++++------- FS/FS/contact_phone.pm | 36 ++++----- FS/FS/cust_main.pm | 16 +++- FS/FS/cust_main/Search.pm | 184 +++++++++++++++++++++++++++++++++++----------- 5 files changed, 262 insertions(+), 94 deletions(-) diff --git a/FS/FS/contact.pm b/FS/FS/contact.pm index 8fcd724a0..da6f2eb99 100644 --- a/FS/FS/contact.pm +++ b/FS/FS/contact.pm @@ -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 diff --git a/FS/FS/contact_email.pm b/FS/FS/contact_email.pm index 1276d8d68..4f787358b 100644 --- a/FS/FS/contact_email.pm +++ b/FS/FS/contact_email.pm @@ -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 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, schema.html from the base documentation. +L, L =cut diff --git a/FS/FS/contact_phone.pm b/FS/FS/contact_phone.pm index 7ba85234a..0eb216668 100644 --- a/FS/FS/contact_phone.pm +++ b/FS/FS/contact_phone.pm @@ -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 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; @@ -154,13 +139,18 @@ sub phonenum_pretty { } +sub contact { + my $self = shift; + qsearchs( 'contact', { 'contactnum' => $self->contactnum } ); +} + =back =head1 BUGS =head1 SEE ALSO -L, schema.html from the base documentation. +L, L =cut diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 3e36c6049..d768f8406 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -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; diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index 70d12c97d..362a6aa1c 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -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, @@ -356,7 +434,7 @@ sub smart_search { %fuzopts ); } - foreach my $field ( 'last', 'company' ) { + foreach my $field ( 'first', 'last', 'company' ) { push @cust_main, FS::cust_main::Search->fuzzy_search( { $field => $value }, %fuzopts ); } @@ -1004,8 +1082,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({ @@ -1035,7 +1115,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 @@ -1088,34 +1175,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 -- cgit v1.2.1 From cf4e690746ef76895e50d057b9e3f0e660f8e21e Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sat, 2 Nov 2013 13:58:12 -0700 Subject: contact search, RT#25687 (also possibly #25583 and #22991) --- FS/FS/cust_main/Search.pm | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index 362a6aa1c..16db71271 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -433,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 ); + 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 + ); } } -- cgit v1.2.1 From 9c2f7147ea99459d69c7264a7a7c7a80f6f3d5a0 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sun, 3 Nov 2013 21:14:56 -0800 Subject: foreign keys, performance improvement, RT#13971 --- FS/FS/Record.pm | 35 +- FS/FS/Schema.pm | 2894 +++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 2286 insertions(+), 643 deletions(-) diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index 71eddc1eb..8ab96cf6a 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -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.42; #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;"; diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 59c2abee9..4e5a58770 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -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.42; #0.42 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; @@ -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,9 +2734,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_void', + }, + { columns => [ 'pkgdiscountnum' ], + table => 'cust_pkg_discount', + }, + ], }, 'discount' => { @@ -2017,9 +2760,14 @@ sub tables_hashref { 'setup', 'char', 'NULL', 1, '', '', #'linked', 'char', 'NULL', 1, '', '', ], - 'primary_key' => 'discountnum', - 'unique' => [], - 'index' => [], # [ 'agentnum' ], ], + 'primary_key' => 'discountnum', + 'unique' => [], + 'index' => [], # [ 'agentnum' ], ], + 'foreign_keys' => [ + { columns => [ 'classnum' ], + table => 'discount_class', + }, + ], }, 'discount_class' => { @@ -2057,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' => { @@ -2070,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', + }, + ], }, @@ -2084,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' => { @@ -2096,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' => { @@ -2108,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' => { @@ -2120,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' => { @@ -2155,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' => { @@ -2170,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' => { @@ -2183,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' => { @@ -2208,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) @@ -2221,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' => { @@ -2266,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' => { @@ -2278,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' => { @@ -2303,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' => { @@ -2315,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' => { @@ -2332,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' => { @@ -2346,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' => { @@ -2386,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', '', '', '', '', @@ -2402,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', '', '', '', '', @@ -2415,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' => { @@ -2481,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' => { @@ -2553,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', '', '', '', '', @@ -2587,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' => { @@ -2600,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', '', '', '', '', @@ -2614,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' => { @@ -2626,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' => { @@ -2642,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' => { @@ -2664,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' => { @@ -2690,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' => { @@ -2716,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' => { @@ -2726,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' => { @@ -2738,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' => { @@ -2751,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' => { @@ -2766,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' => { @@ -2792,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' => { @@ -2804,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' => { @@ -2822,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' => { @@ -2833,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' => { @@ -2850,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' => { @@ -2864,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' => { @@ -2875,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' => { @@ -2886,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' => { @@ -2911,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' => { @@ -2923,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', '', '', '', '', @@ -2963,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' => { @@ -2989,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' => { @@ -3011,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' => { @@ -3037,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' => { @@ -3054,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' => { @@ -3065,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' => { @@ -3079,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' => { @@ -3109,9 +4300,27 @@ 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' => { @@ -3146,9 +4355,14 @@ sub tables_hashref { #downtilt etc? rfpath has profile files for devices/antennas you upload? 'range', 'decimal', 'NULL', '', '', '', #? ], - 'primary_key' => 'sectornum', - 'unique' => [ [ 'towernum', 'sectorname' ], [ 'ip_addr' ], ], - 'index' => [ [ 'towernum' ] ], + 'primary_key' => 'sectornum', + 'unique' => [ [ 'towernum', 'sectorname' ], [ 'ip_addr' ], ], + 'index' => [ [ 'towernum' ] ], + 'foreign_keys' => [ + { columns => [ 'towernum' ], + table => 'tower', + }, + ], }, 'part_virtual_field' => { @@ -3171,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' => { @@ -3191,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' => { @@ -3202,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' => { @@ -3215,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' => { @@ -3227,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' => { @@ -3239,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', '', '', '', '', @@ -3265,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' => { @@ -3276,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' => { @@ -3307,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' => { @@ -3336,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' => { @@ -3358,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 @@ -3382,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' => { @@ -3406,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' => [ @@ -3417,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' => { @@ -3440,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' => { @@ -3469,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' => { @@ -3482,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' => { @@ -3500,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' => { @@ -3526,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' => { @@ -3610,7 +4956,7 @@ sub tables_hashref { ### 'servicecode', 'int', 'NULL', '', '', '', 'quantity_able', 'int', 'NULL', '', '', '', - + ### #and now for our own fields ### @@ -3670,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' => { @@ -3694,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... @@ -3767,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' => { @@ -3790,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' => { @@ -3807,9 +5179,19 @@ sub tables_hashref { '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' => { @@ -3820,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' => { @@ -3841,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' => [ @@ -3852,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' => { @@ -3895,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' => { @@ -3909,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' => { @@ -3920,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' => { @@ -3942,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', '', '', '', '', @@ -3964,7 +5418,7 @@ sub tables_hashref { 'unique' => [], 'index' => [], }, - + 'msa' => { 'columns' => [ 'msanum', 'int', '', '', '', '', @@ -3974,7 +5428,7 @@ sub tables_hashref { 'unique' => [], 'index' => [], }, - + 'rate_center' => { 'columns' => [ 'ratecenternum', 'serial', '', '', '', '', @@ -3994,7 +5448,7 @@ sub tables_hashref { 'unique' => [], 'index' => [], }, - + 'did_order_item' => { 'columns' => [ 'orderitemnum', 'serial', '', '', '', '', @@ -4007,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' => { @@ -4022,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' => { @@ -4047,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' => { @@ -4060,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' => { @@ -4071,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' => { @@ -4084,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? @@ -4100,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' => { @@ -4123,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' => { @@ -4135,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' => { @@ -4146,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' => { @@ -4158,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' => [ @@ -4175,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' => { @@ -4188,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' => { @@ -4206,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' => { @@ -4227,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' => { @@ -4237,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' => { @@ -4281,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' => { @@ -4296,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' => { @@ -4307,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' => { @@ -4323,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' => { @@ -4337,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' => { @@ -4371,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' => { @@ -4394,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' => { @@ -4406,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' => { @@ -4419,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() }, @@ -4444,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' => { @@ -4458,6 +6096,11 @@ sub tables_hashref { 'primary_key' => 'modenum', 'unique' => [ ], 'index' => [ ], + 'foreign_keys' => [ + { columns => [ 'agentnum' ], + table => 'agent', + }, + ], }, 'invoice_conf' => { @@ -4491,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 -- cgit v1.2.1 From b173bfdcca96222fdc1883a2fcbf2fb817001542 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 4 Nov 2013 15:03:22 -0800 Subject: foreign keys, RT#13971 --- FS/FS/Record.pm | 2 +- FS/FS/Schema.pm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index 8ab96cf6a..1a88c3acd 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -12,7 +12,7 @@ use File::Slurp qw( slurp ); use File::CounterFile; use Text::CSV_XS; use DBI qw(:sql_types); -use DBIx::DBSchema 0.42; #for foreign keys +use DBIx::DBSchema 0.43; #0.43 for foreign keys use Locale::Country; use Locale::Currency; use NetAddr::IP; # for validation diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 4e5a58770..f312b5536 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -3,7 +3,7 @@ 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.42; #0.42 for foreign keys +use DBIx::DBSchema 0.43; #0.43 for foreign keys use DBIx::DBSchema::Table; use DBIx::DBSchema::Column; use DBIx::DBSchema::Index; -- cgit v1.2.1 From d655db744becadfcc4913b0fbb2706149dadd34d Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Tue, 5 Nov 2013 22:19:27 -0800 Subject: improve handling of sublocations in TomTom geocoding, #25658, #13763 --- FS/FS/Misc/Geo.pm | 168 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 125 insertions(+), 43 deletions(-) diff --git a/FS/FS/Misc/Geo.pm b/FS/FS/Misc/Geo.pm index 6bd817cfc..bf4840bbd 100644 --- a/FS/FS/Misc/Geo.pm +++ b/FS/FS/Misc/Geo.pm @@ -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,19 @@ sub standardize_tomtom { my $country = code2country($location->{country}); my ($address1, $address2) = ($location->{address1}, $location->{address2}); + my $subloc = ''; + # 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 +465,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"; } - 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 +515,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 +565,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 +642,7 @@ sub normalize_address2 { warn "normalizing '$addr2' to '$result'\n" if $DEBUG > 1; $addr2 = $result; } - $addr2; + ($subloc, $addr2); } @@ -567,5 +650,4 @@ sub normalize_address2 { =cut - 1; -- cgit v1.2.1 From 7f2616837fec94c8d3e2bc255e05a3bb51dc773a Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Tue, 5 Nov 2013 22:19:29 -0800 Subject: in upgrade, correctly migrate census tracts to bill_locations, #25660, #940 --- FS/FS/cust_main/Location.pm | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/FS/FS/cust_main/Location.pm b/FS/FS/cust_main/Location.pm index 5590f8869..6b707b1eb 100644 --- a/FS/FS/cust_main/Location.pm +++ b/FS/FS/cust_main/Location.pm @@ -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,37 @@ 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; + 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 -- cgit v1.2.1 From d86f30add230405cb3ff689846ecce7e639e461b Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 6 Nov 2013 12:14:42 -0800 Subject: TomTom: return a definite error on incomplete matches, #25261 --- FS/FS/Misc/Geo.pm | 11 ++++++-- httemplate/misc/confirm-address_standardize.html | 34 +++++++++++++----------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/FS/FS/Misc/Geo.pm b/FS/FS/Misc/Geo.pm index bf4840bbd..9f6b89bf2 100644 --- a/FS/FS/Misc/Geo.pm +++ b/FS/FS/Misc/Geo.pm @@ -447,6 +447,12 @@ sub standardize_tomtom { 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; @@ -487,8 +493,8 @@ sub standardize_tomtom { } } - if (!$match) { - die "Location not found.\n"; + if ( !$match or !$clean ) { # partial matches are not useful + die "Address not found\n"; } my $tract = ''; if ( defined $match->{censusTract} ) { @@ -642,6 +648,7 @@ sub subloc_address2 { warn "normalizing '$addr2' to '$result'\n" if $DEBUG > 1; $addr2 = $result; } + $addr2 = '' if $addr2 eq $subloc; # if it was entered redundantly ($subloc, $addr2); } diff --git a/httemplate/misc/confirm-address_standardize.html b/httemplate/misc/confirm-address_standardize.html index 33d22195b..2eae011c4 100644 --- a/httemplate/misc/confirm-address_standardize.html +++ b/httemplate/misc/confirm-address_standardize.html @@ -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'} ) { Entered <%$name%> address - Standardized <%$name%> address - % if ( $old{$pre.'company'} ) { <% $old{$pre.'company'} %> - <% $new{$pre.'company'} %> % } <% $old{$pre.'address1'} %> - <% $new{$pre.'address1'} %> + <% $new{$pre.'error'} %> <% $old{$pre.'address2'} %> - <% $new{$pre.'address2'} %> <% $old{$pre.'city'} %>, <% $old{$pre.'state'} %> <% $old{$pre.'zip'} %> - <% $new{$pre.'city'} %>, <% $new{$pre.'state'} %> <% $new{$pre.'zip'} %> - -% } # if addr_clean -% elsif ( $new{$pre.'error'} ) { +% } else { # not an error Entered <%$name%> address + Standardized <%$name%> address + +% if ( !$new{$pre.'addr_clean'} ) { + + + (unverified) +% } + % if ( $old{$pre.'company'} ) { <% $old{$pre.'company'} %> + <% $new{$pre.'company'} %> % } <% $old{$pre.'address1'} %> - <% $new{$pre.'error'} %> + <% $new{$pre.'address1'} %> <% $old{$pre.'address2'} %> + <% $new{$pre.'address2'} %> <% $old{$pre.'city'} %>, <% $old{$pre.'state'} %> <% $old{$pre.'zip'} %> + <% $new{$pre.'city'} %>, <% $new{$pre.'state'} %> <% $new{$pre.'zip'} %> -% } #if error + +% } # if error % } # for $pre %# only do this part if address standardization provided a censustract @@ -88,7 +93,7 @@ Confirm address standardization % } #if censustract -% if ( $new{bill_error} or $new{ship_error} ) { +% if ( grep {$new{$_.'error'}} @prefixes ) { -% } -% else { +% } else {