[freeside-commits] branch master updated. ac01b85bf09d058d3cda097dffbf4e8fce5f4305

Mark Wells mark at 420.am
Thu Oct 31 16:47:35 PDT 2013


The branch, master has been updated
       via  ac01b85bf09d058d3cda097dffbf4e8fce5f4305 (commit)
      from  2e7ce01632012ccc0dd440a8bc37a9ec9bd55fac (commit)

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

- Log -----------------------------------------------------------------
commit ac01b85bf09d058d3cda097dffbf4e8fce5f4305
Author: Mark Wells <mark at freeside.biz>
Date:   Thu Oct 31 16:42:18 2013 -0700

    improved tax report, #23349

diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi
index fc74b54..d86641d 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 40b9ed7..382dbc4 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 d71fcf9..111f22d 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
+&>
 <TD ALIGN="right">
 Download full results<BR>
 as <A HREF="<% $p.'search/report_tax-xls.cgi?'.$cgi->query_string%>">Excel spreadsheet</A>
 </TD>
 
 <STYLE type="text/css">
-td.sectionhead {
+TD.sectionhead {
   background-color: #777777;
   color: #ffffff;
   font-weight: bold;
   text-align: left;
 }
+.grid TH { background-color: #cccccc; padding: 0px 3px 2px }
+.row0 TD { background-color: #eeeeee; padding: 0px 3px 2px; text-align: right}
+.row1 TD { background-color: #ffffff; padding: 0px 3px 2px; text-align: right}
+TD.rowhead { font-weight: bold; text-align: left }
+.bigmath { font-size: large; font-weight: bold; font: sans-serif; text-align: center }
 </STYLE>
-<% include('/elements/table-grid.html') %>
+<& /elements/table-grid.html &>
   <TR>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=9>Sales</TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3>Rate</TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3>Tax owed</TH>
-% unless ( $cgi->param('show_taxclasses') ) { 
-      <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3>Tax invoiced</TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3></TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3>Tax credited</TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3></TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=3>Tax collected</TH>
-% } 
+    <TH ROWSPAN=3></TH>
+    <TH COLSPAN=5>Sales</TH>
+    <TH ROWSPAN=3></TH>
+    <TH ROWSPAN=3>Rate</TH>
+    <TH ROWSPAN=3></TH>
+    <TH ROWSPAN=3>Estimated tax</TH>
+    <TH ROWSPAN=3>Tax invoiced</TH>
+    <TH ROWSPAN=3></TH>
+    <TH ROWSPAN=3>Tax credited</TH>
+    <TH ROWSPAN=3></TH>
+    <TH ROWSPAN=3>Net tax due</TH>
   </TR>
 
   <TR>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2>Total</TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=1>Non-taxable</TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=1>Non-taxable</TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=1>Non-taxable</TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2>Taxable</TH>
+    <TH ROWSPAN=2>Total</TH>
+    <TH ROWSPAN=1>Non-taxable</TH>
+    <TH ROWSPAN=1>Non-taxable</TH>
+    <TH ROWSPAN=1>Non-taxable</TH>
+    <TH ROWSPAN=2>Taxable</TH>
   </TR>
 
-  <TR>
-    <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>(tax-exempt customer)</FONT></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>(tax-exempt package)</FONT></TH>
-    <TH CLASS="grid" BGCOLOR="#cccccc"><FONT SIZE=-1>(monthly exemption)</FONT></TH>
+  <TR STYLE="font-size:small">
+    <TH>(tax-exempt customer)</TH>
+    <TH>(tax-exempt package)</TH>
+    <TH>(monthly exemption)</TH>
   </TR>
 
-% foreach my $class (@pkgclasses ) {
-%   next if @{ $class->{regions} } == 0;
-%   if ( $class->{classname} ) {
-  <TR>
-    <TD COLSPAN=19 CLASS="sectionhead"><% $class->{classname} %></TD>
+% my $row = 0;
+% my $classlink = '';
+% my $descend;
+% $descend = sub {
+%   my ($data, $label) = @_;
+%   if ( ref $data eq 'ARRAY' ) {
+%     # then we've reached the bottom
+%     my (%taxnums, %values);
+%     foreach (@$data) {
+%       $taxnums{ $_->[0] } = $_->[1];
+%       $values{ $_->[0] } = $_->[2];
+%     }
+%     # finally, output
+  <TR CLASS="row<% $row %>">
+%     # Row label
+    <TD CLASS="rowhead"><% $label |h %></TD>
+%     # Total Sales
+%     my $sales = $money_sprintf->(
+%       $values{taxable} +
+%       $values{exempt_cust} +
+%       $values{exempt_pkg} +
+%       $values{exempt_monthly}
+%     );
+%     my %sales_taxnums;
+%     foreach my $x (qw(taxable exempt_cust exempt_pkg exempt_monthly)) {
+%       foreach (split(',', $taxnums{$x})) {
+%         $sales_taxnums{$_} = 1;
+%       }
+%     }
+%     my $sales_taxnums = join(',', keys %sales_taxnums);
+    <TD>
+      <A HREF="<% "$saleslink;$classlink;taxnum=$sales_taxnums" %>">
+        <% $sales %>
+      </A>
+    </TD>
+%     # exemptions
+%     foreach(qw(cust pkg)) {
+    <TD>
+      <A HREF="<% "$saleslink;$classlink;exempt_$_=Y;taxnum=".$taxnums{"exempt_$_"} %>">
+        <% $money_sprintf->($values{"exempt_$_"}) %>
+      </A>
+    </TD>
+%     }
+    <TD>
+      <A HREF="<% "$exemptlink;$classlink;taxnum=".$taxnums{"exempt_monthly"} %>">
+        <% $money_sprintf->($values{"exempt_monthly"}) %>
+      </A>
+    </TD>
+%     # taxable
+    <TD>
+      <A HREF="<% "$saleslink;$classlink;taxable=1;taxnum=$taxnums{taxable}" %>">
+        <% $money_sprintf->($values{taxable}) %>
+      </A>
+    </TD>
+%     # tax rate
+%     my $rate;
+%     foreach(split(',', $taxnums{tax})) {
+%       $rate ||= $taxrates{$_};
+%       if ($rate != $taxrates{$_}) {
+%         $rate = 'variable';
+%         last;
+%       }
+%     }
+%     $rate = sprintf('%.2f', $rate) . '%' if ($rate and $rate ne 'variable');
+    <TD CLASS="bigmath"> × </TD>
+    <TD><% $rate %></TD>
+%     # estimated tax
+    <TD CLASS="bigmath"> = </TD>
+    <TD><% $rate eq 'variable' 
+            ? ''
+            : $money_sprintf->( $values{taxable} * $rate / 100 ) %>
+    </TD>
+%     # invoiced tax
+    <TD>
+      <A HREF="<% "$taxlink;$classlink;taxnum=$taxnums{taxable}" %>">
+        <% $money_sprintf->( $values{tax} ) %>
+      </A>
+    </TD>
+%     # credited tax
+    <TD CLASS="bigmath"> − </TD>
+    <TD>
+      <A HREF="<% "$creditlink;$classlink;taxnum=$taxnums{credited}" %>">
+        <% $money_sprintf->( $values{credited} ) %>
+      </A>
+    </TD>
+%     # net tax due
+    <TD CLASS="bigmath"> = </TD>
+    <TD><% $money_sprintf->( $values{tax} - $values{credited} ) %></TD>
   </TR>
-%   }
 
-% my $bgcolor1 = '#eeeeee';
-% my $bgcolor2 = '#ffffff';
-% my $bgcolor;
-
-% my @regions = @{ $class->{regions} };
-% foreach my $region ( @regions ) {
-%
-%   my $link = '';
-%   if ( $with_pkgclass and length($class->{classnum}) ) {
-%     $link = ';classnum='.$class->{classnum};
-%   } # else we're not breaking down pkg class, or this is the grand total
-%
-%   if ( $region->{'label'} eq $out ) {
-%     $link .= ';out=1';
-%   } elsif ( $region->{'taxnums'} ) {
-%     # might be nicer to specify this as country:state:city
-%     $link .= ';'.join(';', map { "taxnum=$_" } @{ $region->{'taxnums'} });
-%   }
-%
-%   if ( $bgcolor eq $bgcolor1 ) {
-%     $bgcolor = $bgcolor2;
-%   } else {
-%     $bgcolor = $bgcolor1;
-%   }
+%     $row = $row ? 0 : 1;
 %
-%   my $hicolor = $bgcolor;
-%   unless ( $cgi->param('show_taxclasses') ) {
-%     my $diff = abs(   sprintf( '%.2f', $region->{'owed'} )
-%                     - sprintf( '%.2f', $region->{'tax'}  )
-%                   );
-%     if ( $diff > 0.02 ) {
-%       $hicolor = $hicolor eq '#eeeeee' ? '#eeee99' : '#ffffcc';
+%   } else { # we're not at the lowest classification
+%     my @keys = sort { $a <=> $b or $a cmp $b } keys(%$data);
+%     foreach my $key (@keys) {
+%       my $sublabel = join(', ', grep $_, $label, $key);
+%       &{ $descend }($data->{$key}, $sublabel);
 %     }
 %   }
-%
-%
-%   my $td = qq(TD CLASS="grid" BGCOLOR="$bgcolor");
-%   my $tdh = qq(TD CLASS="grid" BGCOLOR="$hicolor");
-%   my $bigmath = '<FONT FACE="sans-serif" SIZE="+1"><B>';
-%   my $bme = '</B></FONT>';
-
-%   if ( $region->{'is_total'} ) {
-    <TR STYLE="font-style: italic">
-      <TD STYLE="text-align: right; padding-right: 1ex; background-color:<%$bgcolor%>">Total</TD>
-%   } else {
-    <TR>
-      <<%$td%>><% $region->{'label'} %></TD>
+% };
+
+% my @pkgclasses = sort { $a <=> $b } keys %data;
+% foreach my $pkgclass (@pkgclasses) {
+%   my $class = FS::pkg_class->by_key($pkgclass) ||
+%               FS::pkg_class->new({ classname => 'Unclassified' });
+  <TBODY>
+%   if ( $breakdown{pkgclass} ) {
+  <TR>
+    <TD COLSPAN=19 CLASS="sectionhead"><% $class->classname %></TD>
+  </TR>
 %   }
-      <<%$td%> ALIGN="right">
-        <A HREF="<% $baselink. $link %>;nottax=1"
-        ><% &$money_sprintf( $region->{'sales'} ) %></A>
-      </TD>
-%   if ( $region->{'label'} eq $out ) {
-      <<%$td%> COLSPAN=12></TD>
-%   } else { #not $out
-      <<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD>
-      <<%$td%> ALIGN="right">
-        <A HREF="<% $baselink. $link %>;nottax=1;exempt_cust=Y"
-        ><% &$money_sprintf( $region->{'exempt_cust'} ) %></A>
-      </TD>
-      <<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD>
-      <<%$td%> ALIGN="right">
-        <A HREF="<% $baselink. $link %>;nottax=1;exempt_pkg=Y"
-        ><% &$money_sprintf( $region->{'exempt_pkg'} ) %></A>
-      </TD>
-      <<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD>
-      <<%$td%> ALIGN="right">
-        <A HREF="<% $exemptlink. $link %>"
-        ><% &$money_sprintf( $region->{'exempt_monthly'} ) %></A>
-        </TD>
-      <<%$td%>><FONT SIZE="+1"><B> = </B></FONT></TD>
-      <<%$td%> ALIGN="right">
-        <A HREF="<% $baselink. $link %>;nottax=1;taxable=1"
-        ><% &$money_sprintf( $region->{'taxable'} ) %></A>
-      </TD>
-      <<%$td%>><% $region->{'label'} eq 'Total' ? '' : "$bigmath X $bme" %></TD>
-      <<%$td%> ALIGN="right"><% $region->{'rate'} %></TD>
-      <<%$td%>><% $region->{'label'} eq 'Total' ? '' : "$bigmath = $bme" %></TD>
-      <<%$tdh%> ALIGN="right">
-        <% &$money_sprintf( $region->{'owed'} ) %>
-      </TD>
-%   } # if !$out
-%   unless ( $cgi->param('show_taxclasses') ) {
-%     my $invlink = $region->{'url_param_inv'}
-%                       ? ';'. $region->{'url_param_inv'}
-%                       : $link;
-
-%     if ( $region->{'label'} eq $out ) {
-        <<%$td%> ALIGN="right">
-          <A HREF="<% $baselink. $invlink %>;istax=1"
-          ><% &$money_sprintf_nonzero( $region->{'tax'} ) %></A>
-        </TD>
-        <<%$td%>></TD>
-        <<%$td%> ALIGN="right">
-          <A HREF="<% $creditlink. $invlink %>;istax=1"
-          ><% &$money_sprintf_nonzero( $region->{'credit'} ) %></A>
-        </TD>
-        <<%$td%> COLSPAN=2></TD>
-%     } else { #not $out
-        <<%$tdh%> ALIGN="right">
-          <A HREF="<% $baselink. $invlink %>;istax=1"
-          ><% &$money_sprintf( $region->{'tax'} ) %></A>
-        </TD>
-        <<%$tdh%>><FONT SIZE="+1"><B> - </B></FONT></TD>
-        <<%$tdh%> ALIGN="right">
-          <A HREF="<% $creditlink. $invlink %>;istax=1"
-          ><% &$money_sprintf( $region->{'credit'} ) %></A>
-        </TD>
-        <<%$tdh%>><FONT SIZE="+1"><B> = </B></FONT></TD>
-        <<%$tdh%> ALIGN="right">
-          <% &$money_sprintf( $region->{'tax'} - $region->{'credit'} ) %>
-        </TD>
-%     }
-%   } # show_taxclasses
-
-    </TR>
-% } # foreach $region
-
-%} # foreach $class
-
+%   $row = 0;
+%   $classlink = "classnum=".($pkgclass || 0) if $breakdown{pkgclass};
+%   &{ $descend }( $data{$pkgclass}, '' );
+%   # and now totals
+  </TBODY>
+  <TBODY CLASS="total">
+%   &{ $descend }( $total{$pkgclass}, 'Total' );
+  </TBODY>
+% } # foreach $pkgclass
 </TABLE>
 
-% if ( $cgi->param('show_taxclasses') ) {
-
-    <BR>
-    <% include('/elements/table-grid.html') %>
-    <TR>
-      <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc">Tax invoiced</TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc">Tax credited</TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
-      <TH CLASS="grid" BGCOLOR="#cccccc">Tax collected</TH>
-    </TR>
-
-%   #some false laziness w/above
-%   foreach my $class (@pkgclasses) {
-%   if ( $class->{classname} ) {
-    <TR>
-      <TD COLSPAN=6 CLASS="sectionhead"><% $class->{classname} %></TD>
-    </TR>
-%   }
-
-%   my $bgcolor1 = '#eeeeee';
-%   my $bgcolor2 = '#ffffff';
-%   my $bgcolor;
-%
-%   foreach my $region ( @{ $class->{base_regions} } ) {
-%
-%     my $link = '';
-%     if ( $with_pkgclass and length($class->{classnum}) ) {
-%       $link = ';classnum='.$class->{classnum};
-%     }
-%
-%     if ( $region->{'label'} eq $out ) {
-%       $link .= ';out=1';
-%     } else {
-%       $link .= ';'. $region->{'url_param'}
-%         if $region->{'url_param'};
-%     }
-%
-%     if ( $bgcolor eq $bgcolor1 ) {
-%       $bgcolor = $bgcolor2;
-%     } else {
-%       $bgcolor = $bgcolor1;
-%     }
-%     my $td = qq(TD CLASS="grid" BGCOLOR="$bgcolor");
-%     my $tdh = qq(TD CLASS="grid" BGCOLOR="$bgcolor");
-%
-%     #?
-%     my $invlink = $region->{'url_param_inv'}
-%                     ? ';'. $region->{'url_param_inv'}
-%                     : $link;
-
-      <TR>
-        <<%$td%>><% $region->{'label'} %></TD>
-%     if ( $region->{'label'} eq $out ) {
-        <<%$td%> ALIGN="right">
-          <A HREF="<% $baselink. $invlink %>;istax=1"
-          ><% &$money_sprintf_nonzero( $region->{'tax'} ) %></A>
-        </TD>
-        <<%$td%>></TD>
-        <<%$td%> ALIGN="right">
-          <A HREF="<% $creditlink. $invlink %>;istax=1"
-          ><% &$money_sprintf_nonzero( $region->{'credit'} ) %></A>
-        </TD>
-        <<%$td%> COLSPAN=2></TD>
-%     } else { #not $out
-        <<%$td%> ALIGN="right">
-          <A HREF="<% $baselink. $link %>;istax=1"
-          ><% &$money_sprintf( $region->{'tax'} ) %></A>
-        </TD>
-        <<%$td%>><FONT SIZE="+1"><B> - </B></FONT></TD>
-        <<%$tdh%> ALIGN="right">
-          <A HREF="<% $creditlink. $invlink %>;istax=1"
-          ><% &$money_sprintf( $region->{'credit'} ) %></A>
-        </TD>
-        <<%$td%>><FONT SIZE="+1"><B> = </B></FONT></TD>
-        <<%$tdh%> ALIGN="right">
-          <% &$money_sprintf( $region->{'tax'} - $region->{'credit'} ) %>
-        </TD>
-      </TR>
-%     } # if $out
-%   } #foreach $region
-% } #foreach $class
-
-  </TABLE>
-
-% } # if show_taxclasses
-
-<% include('/elements/footer.html') %>
-
+<& /elements/footer.html &>
 <%init>
 
 die "access denied"
@@ -287,17 +188,38 @@ my $DEBUG = $cgi->param('debug') || 0;
 
 my $conf = new FS::Conf;
 
-my $out = 'Out of taxable region(s)';
+my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
 
-my %label_opt = ( out => 1 ); #enable 'Out of Taxable Region' label
-$label_opt{with_city} = 1     if $cgi->param('show_cities');
-$label_opt{with_district} = 1 if $cgi->param('show_districts');
+my ($taxname, $country, %breakdown);
 
-$label_opt{with_taxclass} = 1 if $cgi->param('show_taxclasses');
+if ( $cgi->param('taxname') =~ /^([\w\s]+)$/ ) {
+  $taxname = $1;
+} else {
+  die "taxname required"; # UI prevents this
+}
 
-my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
+if ( $cgi->param('country') =~ /^(\w\w)$/ ) {
+  $country = $1;
+} else {
+  die "country required";
+}
 
-my $join_cust =     '     JOIN cust_bill      USING ( invnum  ) 
+# %breakdown: short name => field identifier
+foreach ($cgi->param('breakdown')) {
+  if ( $_ eq 'taxclass' ) {
+    $breakdown{'taxclass'} = 'part_pkg.taxclass';
+  } elsif ( $_ eq 'pkgclass' ) {
+    $breakdown{'pkgclass'} = 'part_pkg.classnum';
+  } elsif ( $_ eq 'city' ) {
+    $breakdown{'city'} = 'cust_main_county.city';
+    $breakdown{'district'} = 'cust_main_county.district';
+  }
+}
+# always break these down
+$breakdown{'state'} = 'cust_main_county.state';
+$breakdown{'county'} = 'cust_main_county.county';
+
+my $join_cust =     '      JOIN cust_bill     USING ( invnum  )
                       LEFT JOIN cust_main     USING ( custnum ) ';
 
 my $join_cust_pkg = $join_cust.
@@ -306,7 +228,7 @@ my $join_cust_pkg = $join_cust.
 
 my $from_join_cust_pkg = " FROM cust_bill_pkg $join_cust_pkg "; 
 
-my $with_pkgclass = $cgi->param('show_pkgclasses');
+# all queries MUST be linked to both cust_bill and cust_main_county
 
 # either or both of these can be used to link cust_bill_pkg to cust_main_county
 my $pkg_tax = "SELECT SUM(amount) as tax_amount, invnum, taxnum, ".
@@ -317,21 +239,32 @@ my $pkg_tax = "SELECT SUM(amount) as tax_amount, invnum, taxnum, ".
 my $pkg_tax_exempt = "SELECT SUM(amount) AS exempt_charged, billpkgnum, taxnum ".
   "FROM cust_tax_exempt_pkg EXEMPT_WHERE GROUP BY billpkgnum, taxnum";
 
-my $where = "WHERE _date >= $beginning AND _date <= $ending ";
+my $where = "WHERE _date >= $beginning AND _date <= $ending ".
+            "AND COALESCE(cust_main_county.taxname,'Tax') = '$taxname' ".
+            "AND cust_main_county.country = '$country'";
 # SELECT/GROUP clauses for first-level queries
-# classnum is a placeholder; they all go in one class in this case.
-my $select = "SELECT NULL AS classnum, cust_main_county.taxnum, ";
-my $group =  "GROUP BY cust_main_county.taxnum";
+my $select = "SELECT ";
+my $group = "GROUP BY ";
+foreach (qw(pkgclass taxclass state county city district)) {
+  if ( $breakdown{$_} ) {
+    $select .= "$breakdown{$_} AS $_, ";
+    $group  .= "$breakdown{$_}, ";
+  } else {
+    $select .= "NULL AS $_, ";
+  }
+}
+$select .= "array_to_string(array_agg(DISTINCT(cust_main_county.taxnum)), ',') AS taxnums, ";
+$group =~ s/, $//;
+
 # SELECT/GROUP clauses for second-level (totals) queries
-my $select_all = "SELECT NULL AS classnum, ";
-my $group_all =  "";
-
-if ( $with_pkgclass ) {
-  $select = "SELECT COALESCE(part_pkg.classnum,0), cust_main_county.taxnum, ";
-  $group =  "GROUP BY part_pkg.classnum, cust_main_county.taxnum";
-  $select_all = "SELECT COALESCE(part_pkg.classnum,0), ";
-  $group_all  = "GROUP BY COALESCE(part_pkg.classnum,0)";
+# breakdown by package class only, if anything
+my $select_all = "SELECT NULL AS pkgclass, ";
+my $group_all = "";
+if ( $breakdown{pkgclass} ) {
+  $select_all = "SELECT $breakdown{pkgclass} AS pkgclass, ";
+  $group_all = "GROUP BY $breakdown{pkgclass}";
 }
+$select_all .= "array_to_string(array_agg(DISTINCT(cust_main_county.taxnum)), ',') AS taxnums, ";
 
 my $agentname = '';
 if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
@@ -356,7 +289,8 @@ my $exempt = "$select SUM(exempt_charged)
   JOIN ($pkg_tax_exempt) AS pkg_tax_exempt
   USING (taxnum)
   JOIN cust_bill_pkg USING (billpkgnum)
-  $join_cust_pkg $where AND $nottax $group";
+  $join_cust_pkg $where AND $nottax
+  $group";
 
 my $all_exempt = "$select_all SUM(exempt_charged)
   FROM cust_main_county
@@ -393,24 +327,19 @@ $sql{taxable} = "$select
   LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt
     ON (pkg_tax_exempt.billpkgnum = cust_bill_pkg.billpkgnum 
         AND pkg_tax_exempt.taxnum = cust_main_county.taxnum)
-  $join_cust_pkg $where AND $nottax $group";
+  $join_cust_pkg $where AND $nottax 
+  $group";
 
-# Here we're going to sum all line items that are taxable _at all_,
-# under any tax.  exempt_charged is the sum of all exemptions for a 
-# particular billpkgnum + taxnum; we take the taxnum that has the 
-# smallest sum of exemptions and subtract that from the charged amount.
 $all_sql{taxable} = "$select_all
-  SUM(cust_bill_pkg.setup + cust_bill_pkg.recur - COALESCE(min_exempt, 0))
-  FROM cust_bill_pkg
-  JOIN (
-    SELECT invnum, pkgnum, MIN(exempt_charged) AS min_exempt
-    FROM ($pkg_tax) AS pkg_tax
-    JOIN cust_bill_pkg USING (invnum, pkgnum)
-    LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt USING (billpkgnum, taxnum)
-    GROUP BY invnum, pkgnum
-  ) AS pkg_is_taxable 
-  USING (invnum, pkgnum)
-  $join_cust_pkg $where AND $nottax $group_all";
+  SUM(cust_bill_pkg.setup + cust_bill_pkg.recur - COALESCE(exempt_charged, 0))
+  FROM cust_main_county
+  JOIN ($pkg_tax) AS pkg_tax USING (taxnum)
+  JOIN cust_bill_pkg USING (invnum, pkgnum)
+  LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt
+    ON (pkg_tax_exempt.billpkgnum = cust_bill_pkg.billpkgnum 
+        AND pkg_tax_exempt.taxnum = cust_main_county.taxnum)
+  $join_cust_pkg $where AND $nottax 
+  $group_all";
 
 $sql{taxable} =~ s/EXEMPT_WHERE//; # unrestricted
 $all_sql{taxable} =~ s/EXEMPT_WHERE//;
@@ -428,7 +357,7 @@ my $taxfrom = " FROM cust_bill_pkg
                 LEFT JOIN cust_bill_pkg_tax_location USING ( billpkgnum )
                 LEFT JOIN cust_main_county USING ( taxnum )";
 
-if ( $with_pkgclass ) {
+if ( $breakdown{pkgclass} ) {
   # If we're not grouping by package class, this is unnecessary, and
   # probably really expensive.
   $taxfrom .= "
@@ -439,17 +368,14 @@ if ( $with_pkgclass ) {
 }
 
 my $istax = "cust_bill_pkg.pkgnum = 0";
-my $named_tax =
-  "COALESCE(taxname,'Tax') = COALESCE(cust_bill_pkg.itemdesc,'Tax')";
 
 $sql{tax} = "$select SUM(cust_bill_pkg_tax_location.amount)
              $taxfrom
-             $where AND $istax AND $named_tax
+             $where AND $istax
              $group";
 
-$all_sql{tax} = "$select_all SUM(cust_bill_pkg.setup)
-             FROM cust_bill_pkg
-             $join_cust
+$all_sql{tax} = "$select_all SUM(cust_bill_pkg_tax_location.amount)
+             $taxfrom
              $where AND $istax
              $group_all";
 
@@ -463,325 +389,86 @@ my $creditwhere = $where .
 
 $sql{credit} = "$select SUM(cust_credit_bill_pkg.amount)
                 $creditfrom
-                $creditwhere AND $istax AND $named_tax
+                $creditwhere AND $istax
                 $group";
 
 $all_sql{credit} = "$select_all SUM(cust_credit_bill_pkg.amount)
-                FROM cust_credit_bill_pkg
-                JOIN cust_bill_pkg USING (billpkgnum)
-                $join_cust
-                $where AND $istax
+                $creditfrom
+                $creditwhere AND $istax
                 $group_all";
 
-if ( $with_pkgclass ) {
-  # the slightly more complicated version, with lots of joins that are 
-  # unnecessary if you're not breaking down by package class
-  $all_sql{tax} = "$select_all SUM(cust_bill_pkg_tax_location.amount)
-             $taxfrom
-             $where AND $istax
-             $group_all";
-
-  $all_sql{credit} = "$select_all SUM(cust_credit_bill_pkg.amount)
-                      $creditfrom
-                      $creditwhere AND $istax
-                      $group_all";
-}
-
-# "out of taxable region" sales
-$all_sql{out_sales} = 
-  "$select_all SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)
-  FROM (cust_bill_pkg $join_cust_pkg)
-  LEFT JOIN ($pkg_tax) AS pkg_tax USING (invnum, pkgnum)
-  LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt USING (billpkgnum)
-  $where AND $nottax
-  AND pkg_tax.taxnum IS NULL AND pkg_tax_exempt.taxnum IS NULL
-  $group_all"
-;
-
-$all_sql{out_sales} =~ s/EXEMPT_WHERE//;
-
 my %data;
 my %total;
+my %taxclass_name = { '' => '' };
+if ( $breakdown{taxclass} ) {
+  $taxclass_name{$_->taxclassnum} = $_->taxclass
+    foreach qsearch('tax_class');
+  $taxclass_name{''} = 'Unclassified';
+}
 foreach my $k (keys(%sql)) {
   my $stmt = $sql{$k};
   warn "\n".uc($k).":\n".$stmt."\n" if $DEBUG;
   my $sth = dbh->prepare($stmt);
-  # three columns: classnum, taxnum, value
+  # eight columns: pkgclass, taxclass, state, county, city, district
+  # taxnums (comma separated), value
+  # *sigh*
   $sth->execute 
     or die "failed to execute $k query: ".$sth->errstr;
   while ( my $row = $sth->fetchrow_arrayref ) {
-    $data{$k}{$row->[0]}{$row->[1]} = $row->[2];
+    my $bin = $data
+              {$row->[0]}
+              {$taxclass_name{$row->[1]}}
+              {$row->[2]}
+              {$row->[3] ? $row->[3] . ' County' : ''}
+              {$row->[4]}
+              {$row->[5]}
+            ||= [];
+    push @$bin, [ $k, $row->[6], $row->[7] ];
   }
 }
 warn "DATA:\n".Dumper(\%data) if $DEBUG > 1;
 
 foreach my $k (keys %all_sql) {
-  warn "\n".$all_sql{$k}."\n" if $DEBUG;
+  warn "\nTOTAL ".uc($k).":\n".$all_sql{$k}."\n" if $DEBUG;
   my $sth = dbh->prepare($all_sql{$k});
-  # two columns: classnum, value
+  # three columns: pkgclass, taxnums (comma separated), value
   $sth->execute 
     or die "failed to execute $k totals query: ".$sth->errstr;
   while ( my $row = $sth->fetchrow_arrayref ) {
-    $total{$k}{$row->[0]} = $row->[1];
-  }
-}
-warn "TOTALS:\n".Dumper(\%total);# if $DEBUG > 1;
-# so $data{tax}, for example, is now a hash with one entry
-# for each classnum, containing a hash with one entry for each
-# taxnum, containing the tax billed on that taxnum.
-# if with_pkgclass is off, then the classnum is always null.
-
-# integrity checks
-# unlinked tax collected
-my $out_tax_sql =
-  "SELECT SUM(cust_bill_pkg.setup)
-  FROM (cust_bill_pkg $join_cust)
-  LEFT JOIN cust_bill_pkg_tax_location USING (billpkgnum)
-  $where AND $istax AND cust_bill_pkg_tax_location.billpkgnum IS NULL"
-;
-my $unlinked_tax = FS::Record->scalar_sql($out_tax_sql);
-# unlinked tax credited
-my $out_credit_sql =
-  "SELECT SUM(cust_credit_bill_pkg.amount)
-  FROM cust_credit_bill_pkg
-  JOIN cust_bill_pkg USING (billpkgnum)
-  $join_cust
-  $where AND $istax AND cust_credit_bill_pkg.billpkgtaxlocationnum IS NULL"
-;
-my $unlinked_credit = FS::Record->scalar_sql($out_credit_sql);
-
-# all sales
-my $all_sales = FS::Record->scalar_sql(
-  "SELECT SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)
-  FROM cust_bill_pkg $join_cust $where AND $nottax"
-);
-
-#tax-report_groups filtering
-my($group_op, $group_value) = ( '', '' );
-if ( $cgi->param('report_group') =~ /^(=|!=) (.*)$/ ) {
-  ( $group_op, $group_value ) = ( $1, $2 );
-}
-my $group_test = sub { # to be applied to a tax label
-  my $label = shift;
-  return 1 unless $group_op; #in case we get called inadvertantly
-  if ( $label eq $out ) { #don't display "out of taxable region" in this case
-    0;
-  } elsif ( $group_op eq '=' ) {
-    $label =~ /^$group_value/;
-  } elsif ( $group_op eq '!=' ) {
-    $label !~ /^$group_value/;
-  } else {
-    die "guru meditation #00de: group_op $group_op\n";
+    my $bin = $total{$row->[0]} ||= [];
+    push @$bin, [ $k, $row->[1], $row->[2] ];
   }
-};
-
-my @pkgclasses;
-if ($with_pkgclass) {
-  @pkgclasses = qsearch('pkg_class', {});
-  push @pkgclasses, FS::pkg_class->new({
-    classnum  => '0',
-    classname => 'Unclassified',
-  });
-} else {
-  @pkgclasses = ( FS::pkg_class->new({
-    classnum  => '',
-    classname => '',
-  }) );
 }
-my %pkgclass_data;
-
-foreach my $class (@pkgclasses) {
-  my $classnum = $class->classnum;
-  my $classname = $class->classname;
-
-  # if show_taxclasses is on, %base_regions will contain the same data
-  # as %regions, but with taxclasses merged together (and ignoring report_group
-  # filtering).
-  my (%regions, %base_regions);
-
-  my @loc_params = qw(country state county);
-  push @loc_params, 'city' if $cgi->param('show_cities');
-  push @loc_params, 'district' if $cgi->param('show_districts');
-
-  foreach my $r ( qsearch({ 'table'     => 'cust_main_county', })) {
-    my $taxnum = $r->taxnum;
-    # set up a %regions entry for this region's tax label
-    my $label = $r->label(%label_opt);
-    next if $label eq $out;
-    $regions{$label} ||= { label => $label };
-
-    $regions{$label}->{$_} = $r->get($_) foreach @loc_params;
-    $regions{$label}->{taxnums} ||= [];
-    push @{ $regions{$label}->{taxnums} }, $r->taxnum;
-
-    my %x; # keys are data items (like 'tax', 'exempt_cust', etc.)
-    foreach my $k (keys %data) {
-      next unless exists($data{$k}{$classnum}{$taxnum});
-      $x{$k} = $data{$k}{$classnum}{$taxnum};
-      $regions{$label}{$k} += $x{$k};
-      if ( $k eq 'taxable' or $k =~ /^exempt/ ) {
-        $regions{$label}->{'sales'} += $x{$k};
-      }
-    }
-
-    my $owed = $data{'taxable'}{$classnum}{$taxnum} * ($r->tax/100);
-    $regions{$label}->{'owed'} += $owed;
-    $total{'owed'}{$classnum} += $owed;
-
-    if ( defined($regions{$label}->{'rate'})
-         && $regions{$label}->{'rate'} != $r->tax.'%' ) {
-      $regions{$label}->{'rate'} = 'variable';
-    } else {
-      $regions{$label}->{'rate'} = $r->tax.'%';
-    }
-
-    if ( $cgi->param('show_taxclasses') ) {
-      my $base_label = $r->label(%label_opt, 'with_taxclass' => 0);
-      $base_regions{$base_label} ||=
-      {
-        label   => $base_label,
-        tax     => 0,
-        credit  => 0,
-      };
-      $base_regions{$base_label}->{tax}    += $x{tax};
-      $base_regions{$base_label}->{credit} += $x{credit};
-    }
+warn "TOTALS:\n".Dumper(\%total) if $DEBUG > 1;
 
-  }
-
-  my @regions = map { $_->{label} }
-    sort {
-      ($b eq $out) <=> ($a eq $out)
-      or $a->{country} cmp $b->{country}
-      or $a->{state}   cmp $b->{state}
-      or $a->{county}  cmp $b->{county}
-      or $a->{city}    cmp $b->{city}
-    } 
-    grep { $_->{sales} > 0 or $_->{tax} > 0 or $_->{credit} > 0 }
-    values %regions;
-
-  #tax-report_groups filtering
-  @regions = grep &{$group_test}($_), @regions
-    if $group_op;
-
-  #calculate totals
-  my %taxclasses = ();
-  my %county = ();
-  my %state = ();
-  my %country = ();
-  foreach my $label (@regions) {
-    $taxclasses{$regions{$_}->{'taxclass'}} = 1
-      if $regions{$_}->{'taxclass'};
-    $county{$regions{$_}->{'county'}} = 1;
-    $state{$regions{$_}->{'state'}} = 1;
-    $country{$regions{$_}->{'country'}} = 1;
-  }
-
-  my $total_url_param = '';
-  my $total_url_param_invoiced = '';
-  if ( $group_op ) {
-
-    my @country = keys %country;
-    warn "WARNING: multiple countries on this grouped report; total links broken"
-      if scalar(@country) > 1;
-    my $country = $country[0];
-
-    my @state = keys %state;
-    warn "WARNING: multiple countries on this grouped report; total links broken"
-      if scalar(@state) > 1;
-    my $state = $state[0];
-
-    $total_url_param_invoiced =
-    $total_url_param =
-      'report_group='.uri_escape("$group_op $group_value").';'.
-      join(';', map 'taxclass='.uri_escape($_), keys %taxclasses );
-    $total_url_param .= ';'.
-      "country=$country;state=".uri_escape($state).';'.
-      join(';', map 'county='.uri_escape($_), keys %county ) ;
-
-  }
-
-  #ordering
-  @regions =
-    map $regions{$_},
-    sort { $a cmp $b }
-    @regions;
-
-  my @base_regions =
-    map $base_regions{$_},
-    sort { $a cmp $b }
-    keys %base_regions;
-
-  #add "Out of taxable" and total lines
-  if ( $total{out_sales}{$classnum} ) {
-    my %out = (
-      'sales' => $total{out_sales}{$classnum},
-      'label' => $out,
-      'rate' => ''
-    );
-    push @regions, \%out;
-    push @base_regions, \%out;
-  }
-
-  if ( @regions ) {
-    my %class_total = map { $_ => $total{$_}{$classnum} } keys(%total);
-    $class_total{is_total} = 1;
-    $class_total{sales} = sum(
-      @class_total{ 'taxable',
-                    'out_sales',
-                    grep(/^exempt/, keys %class_total) }
-    );
-
-    push @regions,      \%class_total;
-    push @base_regions, \%class_total;
-  }
-
-  $pkgclass_data{$classname} = {
-    classnum      => $classnum,
-    classname     => $classname,
-    regions       => \@regions,
-    base_regions  => \@base_regions,
-  };
-}
-
-if ( $with_pkgclass ) {
-  my $class_zero = delete( $pkgclass_data{'Unclassified'} );
-  @pkgclasses = map { $pkgclass_data{$_} }
-                sort { $a cmp $b }
-                keys %pkgclass_data;
-  push @pkgclasses, $class_zero;
-
-  my %grand_total = map {
-    $_ => sum( values(%{ $total{$_} }) )
-  } keys(%total);
-
-  $grand_total{sales} = $all_sales;
-
-  push @pkgclasses, {
-    classnum      => '',
-    classname     => 'Total',
-    regions       => [ \%grand_total ],
-    base_regions  => [ \%grand_total ],
-  }
-} else {
-  @pkgclasses = $pkgclass_data{''};
-}
-
-#-- 
+# $data{$pkgclass}{$taxclass}{$state}{$county}{$city}{$district} = [
+#   [ 'taxable',     taxnums, amount ],
+#   [ 'exempt_cust', taxnums, amount ],
+#   ...
+# ]
+# non-requested grouping levels simply collapse into key = ''
 
 my $money_char = $conf->config('money_char') || '$';
 my $money_sprintf = sub {
   $money_char. sprintf('%.2f', shift );
 };
-my $money_sprintf_nonzero = sub {
-  $_[0] == 0 ? '' : &$money_sprintf($_[0])
-};
 
 my $dateagentlink = "begin=$beginning;end=$ending";
 $dateagentlink .= ';agentnum='. $cgi->param('agentnum')
   if length($agentname);
-my $baselink   = $p. "search/cust_bill_pkg.cgi?$dateagentlink";
+my $saleslink  = $p. "search/cust_bill_pkg.cgi?$dateagentlink;nottax=1";
+my $taxlink    = $p. "search/cust_bill_pkg.cgi?$dateagentlink;istax=1";
 my $exemptlink = $p. "search/cust_tax_exempt_pkg.cgi?$dateagentlink";
-my $creditlink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;credit=1";
+my $creditlink = $p. "search/cust_bill_pkg.cgi?$dateagentlink;credit=1;istax=1";
+
+my %taxrates;
+foreach my $tax (
+  qsearch('cust_main_county', {
+            country => $country,
+            tax => { op => '>', value => 0 }
+          }) )
+  {
+  $taxrates{$tax->taxnum} = $tax->tax;
+}
 
 </%init>
diff --git a/httemplate/search/report_tax.html b/httemplate/search/report_tax.html
index 8a207aa..20aa07f 100755
--- a/httemplate/search/report_tax.html
+++ b/httemplate/search/report_tax.html
@@ -4,68 +4,34 @@
 
 <TABLE>
 
-% if ( $conf->config('tax-report_groups') ) {
-%   my @lines = $conf->config('tax-report_groups');
-    
-  <TR>
-    <TD ALIGN="right">Tax group</TD>
-    <TD>
-      <SELECT NAME="report_group">
-
-        <OPTION VALUE="">all</OPTION>
-
-%       foreach my $line ( @lines ) {
-%         $line =~ /^\s*(.+)\s+(=|!=)\s+(.*)\s*$/ #or next;
-%           or do { warn "bad report_group line: $line\n"; next; };
-%         my($label, $op, $value) = ($1, $2, $3);
-
-          <OPTION VALUE="<% "$op $value" %>"><% $label %></OPTION>
-%       }
-
-      </SELECT>
-    </TD>
-  </TR>
-
-% }
-
- <% include( '/elements/tr-select-agent.html', 'disable_empty'=>0 ) %>
-
- <% include( '/elements/tr-input-beginning_ending.html' ) %>
-
-%    if ( $city ) {
-   <TR>
-     <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_cities" VALUE="1" onclick="toggle_show_cities(this)"></TD>
-     <TD>Show cities</TD>
-   </TR>
-   <TR>
-     <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_districts" VALUE="1" DISABLED></TD>
-     <TD>Show districts</TD>
-   </TR>
-  <SCRIPT TYPE="text/javascript">
-  function toggle_show_cities() {
-    what = document.getElementsByName('show_cities')[0];
-    what.form.show_districts.disabled = !what.checked;
-    what.form.show_districts.checked  = what.checked;
-  }
-  toggle_show_cities();
-  </SCRIPT>
-% } 
-
-%    if ( $conf->exists('enable_taxclasses') ) {
-   <TR>
-     <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_taxclasses" VALUE="1"></TD>
-     <TD>Show tax classes</TD>
-   </TR>
-% } 
-
-% my @pkg_class = qsearch('pkg_class', {});
-% if ( @pkg_class ) {
-   <TR>
-     <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_pkgclasses" VALUE="1"></TD>
-     <TD>Show package classes</TD>
-   </TR>
-% } 
-
+  <& /elements/tr-select-agent.html, 'disable_empty'=>0 &>
+
+  <& /elements/tr-input-beginning_ending.html &>
+
+  <& /elements/tr-select.html,
+    'label'         => 'Country',
+    'field'         => 'country',
+    'options'       => \@countries,
+    'curr_value'    => ($conf->config('countrydefault') || 'US'),
+  &>
+
+  <& /elements/tr-select.html,
+    'label'         => 'For tax named ',
+    'field'         => 'taxname',
+    'options'       => \@taxnames,
+    'disable_empty' => 1,
+  &>
+
+  <& /elements/tr-checkbox-multiple.html,
+    'label'         => 'Break down by ',
+    'field'         => 'breakdown',
+    'options'       => \@breakdown,
+    'option_labels' => {
+      taxclass  => 'Tax class',
+      pkgclass  => 'Package class',
+      city      => 'City',
+    },
+  &>
 </TABLE>
 
 <BR><INPUT TYPE="submit" VALUE="Get Report">
@@ -80,12 +46,23 @@ die "access denied"
 
 my $conf = new FS::Conf;
 
-my $city_sql = "SELECT COUNT(*) FROM cust_main_county
-                  WHERE city != '' AND city IS NOT NULL
-                  LIMIT 1";
-
-my $city_sth = dbh->prepare($city_sql) or die dbh->errstr;
-$city_sth->execute or die $city_sth->errstr;
-my $city = $city_sth->fetchrow_arrayref->[0];
+my $sth = dbh->prepare('SELECT DISTINCT(COALESCE(taxname, \'Tax\')) FROM cust_main_county');
+$sth->execute or die $sth->errstr;
+my @taxnames = map { $_->[0] } @{ $sth->fetchall_arrayref };
+
+$sth = dbh->prepare('SELECT DISTINCT(country) FROM cust_location');
+$sth->execute or die $sth->errstr;
+my @countries = map { $_->[0] } @{ $sth->fetchall_arrayref };
+
+my @breakdown;
+if ( $conf->exists('enable_taxclasses') ) {
+  push @breakdown, 'taxclass';
+}
+if ( FS::pkg_class->count() > 0 ) {
+  push @breakdown, 'pkgclass';
+}
+if ( FS::cust_main_county->count("city is not null and city != ''") > 0 ) {
+  push @breakdown, 'city';
+}
 
 </%init>

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

Summary of changes:
 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(-)




More information about the freeside-commits mailing list