+ # If we're showing exempt items, we need to find those with
+ # cust_tax_exempt_pkg records matching the selected taxes.
+ # If we're showing taxable items, we need to find those with
+ # cust_bill_pkg_tax_location records. We also need to find the
+ # exemption records so that we can show the taxable amount.
+ # If we're showing all items, we need the union of those.
+ # If we're showing 'out' (items that aren't region/class taxable),
+ # then we need the set of all items minus the union of those.
+
+ # Always exclude cust_tax_exempt_pkg records with non-NULL creditbillpkgnum.
+
+ if ( $cgi->param('out') ) {
+ # separate from the rest, in that we're not going to join cust_main_county
+ # in the outer query
+
+ my @exclude = ( 'cust_tax_exempt_pkg.billpkgnum',
+ 'cust_bill_pkg_tax_location.taxable_billpkgnum'
+ );
+ foreach my $col (@exclude) {
+ my ($table) = split(/\./, $col);
+ my $this_where = 'WHERE ' . join(' AND ',
+ "$col = cust_bill_pkg.billpkgnum",
+ @tax_where
+ );
+
+ push @where,
+ "NOT EXISTS(SELECT 1 FROM $table
+ JOIN cust_main_county USING (taxnum)
+ $this_where
+ )";
+ }
+
+ } else {
+ # everything that returns things joined to a tax definition
+
+ if ( @exempt_where or @tax_where or $cgi->param('taxable') ) {
+
+ push @exempt_where, "cust_tax_exempt_pkg.creditbillpkgnum IS NULL";
+
+ # process exemption restrictions, including @tax_where
+ my $exempt_sub = 'SELECT SUM(amount) as exempt_amount, billpkgnum
+ FROM cust_tax_exempt_pkg JOIN cust_main_county USING (taxnum)';
+
+ $exempt_sub .= ' WHERE '.join(' AND ', @tax_where, @exempt_where);
+
+ $exempt_sub .= ' GROUP BY billpkgnum';
+
+ $join_pkg .= " LEFT JOIN ($exempt_sub) AS item_exempt
+ ON (cust_bill_pkg.billpkgnum = item_exempt.billpkgnum)";
+
+ }
+
+ if ( @tax_where or $cgi->param('taxable') ) {
+ # process tax restrictions
+ unshift @tax_where,
+ 'cust_main_county.tax > 0';
+
+ my $tax_sub = "SELECT taxable_billpkgnum
+ FROM cust_bill_pkg_tax_location
+ JOIN cust_main_county USING (taxnum)
+ WHERE ". join(' AND ', @tax_where).
+ " GROUP BY taxable_billpkgnum";
+
+ $join_pkg .= " LEFT JOIN ($tax_sub) AS item_tax
+ ON (cust_bill_pkg.billpkgnum = item_tax.taxable_billpkgnum)"
+ }
+
+ # now do something with that
+ if ( $cgi->param('taxable') ) {
+ # taxable query: needs sale amount - exempt amount
+ my $taxable = 'cust_bill_pkg.setup + cust_bill_pkg.recur '.
+ '- COALESCE(item_exempt.exempt_amount, 0)';
+
+ push @where, "item_tax.taxable_billpkgnum IS NOT NULL";
+ push @select, "($taxable) AS taxable_amount";
+ push @peritem, 'taxable_amount';
+ push @peritem_desc, 'Taxable';
+ push @total, "SUM($taxable)";
+ push @total_desc, "$money_char%.2f taxable";
+
+ } elsif ( $cgi->param('exempt_cust') or $cgi->param('exempt_pkg') ) {
+
+ push @where, 'item_exempt.billpkgnum IS NOT NULL';
+ push @select, 'item_exempt.exempt_amount';
+ push @peritem, 'exempt_amount';
+ push @peritem_desc, 'Exempt';
+ push @total, 'SUM(exempt_amount)';
+ push @total_desc, "$money_char%.2f tax-exempt";
+
+ } elsif ( @tax_where ) {
+ # union of taxable + all exempt_ cases
+ push @where,
+ '(item_tax.taxable_billpkgnum IS NOT NULL OR item_exempt.billpkgnum IS NOT NULL)';
+
+ }
+
+ } # handle all joins to cust_main_county
+
+ # setup/recur/usage separation
+ my %charges = map { $_ => 1 } split('', $cgi->param('charges') || 'SRU');
+
+ if ( $charges{S} and $charges{R} and $charges{U} ) {
+ # in this case, show discounts
+
+ $join_pkg .= ' JOIN (
+ SELECT billpkgnum, COALESCE(SUM(amount), 0) AS discounted
+ FROM cust_bill_pkg_discount RIGHT JOIN cust_bill_pkg USING (billpkgnum)
+ GROUP BY billpkgnum
+ ) AS _discount ON (cust_bill_pkg.billpkgnum = _discount.billpkgnum)
+ ';
+ push @select, '_discount.discounted';
+
+ push @peritem, 'discounted';
+ push @peritem_desc, 'Discount';
+ push @total, 'SUM(cust_bill_pkg.setup + cust_bill_pkg.recur + discounted)',
+ 'SUM(discounted)',
+ 'SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)';
+ push @total_desc, "$money_char%.2f gross sales",
+ "− $money_char%.2f discounted",
+ "= $money_char%.2f invoiced";
+
+ } elsif ( $charges{R} and $charges{U} ) {
+
+ # hide rows with no recurring fee, and show the sum of recurring fees only
+ push @where, 'cust_bill_pkg.recur > 0';
+ push @total, "SUM(cust_bill_pkg.recur)";
+ push @total_desc, "$money_char%.2f recurring";
+
+ } elsif ( $charges{R} and !$charges{U} ) {
+
+ my $recur_no_usage = FS::cust_bill_pkg->charged_sql('', '',
+ setuprecur => 'recur', no_usage => 1);
+ push @select, "($recur_no_usage) AS recur_no_usage";
+ $peritem[1] = 'recur_no_usage';
+ $peritem_desc[1] = 'Recurring charges (excluding usage)';
+ push @total, "SUM($recur_no_usage)";
+ push @total_desc, "$money_char%.2f recurring";
+ if ( !$charges{S} ) {
+ push @where, "($recur_no_usage) > 0";
+ }
+
+ } elsif ( !$charges{R} and $charges{U} ) {
+
+ my $usage = FS::cust_bill_pkg->usage_sql();
+ push @select, "($usage) AS _usage";
+ # there's already a method named 'usage'
+ $peritem[1] = '_usage';
+ $peritem_desc[1] = 'Usage charge';
+ push @total, "SUM($usage)";
+ push @total_desc, "$money_char%.2f usage charges";
+ if ( !$charges{S} ) {
+ push @where, "($usage) > 0";
+ }
+
+ } elsif ( $charges{S} ) {
+
+ push @where, "cust_bill_pkg.setup > 0";
+ push @total, "SUM(cust_bill_pkg.setup)";
+ push @total_desc, "$money_char%.2f setup";
+
+ } # else huh? you have to have SOME charges
+
+} elsif ( $cgi->param('istax') ) {
+
+ # ensure that it is a tax:
+ push @where, 'cust_bill_pkg.pkgnum = 0',
+ 'cust_bill_pkg.feepart IS NULL';
+
+ # We MUST NOT join cust_bill_pkg to any table that it's 1:many to.
+ # Otherwise we get duplication of the cust_bill_pkg records,
+ # inaccurate totals, nonsensical paging behavior, etc.
+ # We CAN safely join it to a subquery that has unique billpkgnums, and
+ # that's what we'll do.
+
+ my $tax_subquery;
+ my @tax_where;
+
+ # tax location when using tax_rate_location
+ if ( $cgi->param('vendortax') ) {
+
+ $tax_subquery = '
+ SELECT billpkgnum, SUM(amount) as tax_total
+ FROM cust_bill_pkg_tax_rate_location AS tax
+ JOIN tax_rate_location USING (taxratelocationnum)
+ ';
+ foreach (qw( state county city locationtaxid)) {
+ if ( scalar($cgi->param($_)) ) {
+ my $place = dbh->quote( $cgi->param($_) );
+ push @tax_where, "tax_rate_location.$_ = $place";
+ }
+ }
+
+ # itemdesc, for breakdown from the vendor tax report
+ # (this is definitely used)
+ if ( $cgi->param('itemdesc') ) {
+ if ( $cgi->param('itemdesc') eq 'Tax' ) {
+ push @where, "($itemdesc = 'Tax' OR $itemdesc is null)";
+ } else {
+ push @where, "$itemdesc = ". dbh->quote($cgi->param('itemdesc'));
+ }
+ }
+
+ } else { # the internal-tax case
+
+ my $tax_select = 'SELECT tax.billpkgnum, SUM(tax.amount) as tax_total';
+ my $tax_from = ' FROM cust_bill_pkg_tax_location AS tax JOIN cust_main_county USING (taxnum)';
+
+ # package classnum
+ if ( grep { $_ eq 'classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('classnum');
+ $tax_from .= '
+ JOIN cust_bill_pkg AS taxed_item
+ ON (tax.taxable_billpkgnum = taxed_item.billpkgnum)
+ LEFT JOIN cust_pkg AS taxed_pkg ON (taxed_item.pkgnum = taxed_pkg.pkgnum)
+ LEFT JOIN part_pkg AS taxed_part_pkg ON (taxed_pkg.pkgpart = taxed_part_pkg.pkgpart)
+ LEFT JOIN part_fee AS taxed_part_fee ON (taxed_item.feepart = taxed_part_fee.feepart)
+ ';
+ push @tax_where,
+ "COALESCE(taxed_part_pkg.classnum, taxed_part_fee.classnum,0) IN ( ".
+ join(',', @classnums ).
+ ' )'
+ if @classnums;
+ }
+
+ # taxclass
+ if ( $cgi->param('taxclassNULL') ) {
+ push @tax_where, 'cust_main_county.taxclass IS NULL';
+ }
+
+ # taxname
+ if ( $cgi->param('taxnameNULL') ) {
+ push @tax_where, 'cust_main_county.taxname IS NULL OR '.
+ 'cust_main_county.taxname = \'Tax\'';
+ } elsif ( $cgi->param('taxname') ) {
+ push @tax_where, 'cust_main_county.taxname = '.
+ dbh->quote($cgi->param('taxname'));
+ }
+
+ # itemdesc, for breakdown from the vendor tax report
+ # (is this even used? vendor tax report shouldn't use cust_bill_pkg.cgi)
+ if ( $cgi->param('itemdesc') ) {
+ if ( $cgi->param('itemdesc') eq 'Tax' ) {
+ push @where, "($itemdesc = 'Tax' OR $itemdesc is null)";
+ } else {
+ push @where, "$itemdesc = ". dbh->quote($cgi->param('itemdesc'));
+ }
+ }
+
+ # specific taxnums (the usual way)
+ if ( $cgi->param('taxnum') =~ /^([\d,]+)$/) {
+ push @tax_where, "cust_main_county.taxnum IN ($1)";
+ }
+
+ $tax_subquery = "$tax_select $tax_from";
+
+ } # end of internal-tax case
+
+ if (@tax_where) {
+ $tax_subquery .= '
+ WHERE ' . join(' AND ', map "($_)", @tax_where);