X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=httemplate%2Fsearch%2Fcust_bill_pkg.cgi;h=d86641de392a9c451722e18e52cc16875af130e4;hb=ac01b85bf09d058d3cda097dffbf4e8fce5f4305;hp=74efe4f7e969937ed55a1038630c89f5c04849cb;hpb=64e7037dd42a240f095e76ea7cba007dd1340bc5;p=freeside.git diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index 74efe4f7e..d86641de3 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -1,47 +1,87 @@ -<% include( 'elements/search.html', - 'title' => 'Line items', - 'name' => 'line items', +<& elements/search.html, + 'title' => emt('Line items'), + 'name' => emt('line items'), 'query' => $query, 'count_query' => $count_query, - 'count_addl' => [ $money_char. '%.2f total', ], + 'count_addl' => \@total_desc, 'header' => [ - '#', - 'Description', - 'Setup charge', - 'Recurring charge', - 'Invoice', - 'Date', + @pkgnum_header, + emt('Pkg Def'), + emt('Description'), + @post_desc_header, + @peritem_desc, + @currency_desc, + emt('Invoice'), + emt('Date'), + emt('Paid'), + emt('Credited'), FS::UI::Web::cust_header(), ], 'fields' => [ - 'billpkgnum', + @pkgnum, sub { $_[0]->pkgnum > 0 - ? $_[0]->get('pkg') + # possibly use override.pkg but i think this correct + ? $_[0]->get('pkgpart') + : '' + }, + sub { $_[0]->pkgnum > 0 + # possibly use override.pkg but i think this correct + ? $_[0]->get('pkg') : $_[0]->get('itemdesc') }, + @post_desc, #strikethrough or "N/A ($amount)" or something these when # they're not applicable to pkg_tax search - sub { sprintf($money_char.'%.2f', shift->setup ) }, - sub { sprintf($money_char.'%.2f', shift->recur ) }, + @peritem_sub, + @currency_sub, 'invnum', sub { time2str('%b %d %Y', shift->_date ) }, + sub { sprintf($money_char.'%.2f', shift->get('pay_amount')) }, + sub { sprintf($money_char.'%.2f', shift->get('credit_amount')) }, \&FS::UI::Web::cust_fields, ], - 'links' => [ + 'sort_fields' => [ + @pkgnum_null, '', '', + @post_desc_null, + @peritem, + @currency, + 'invnum', + '_date', + #'pay_amount', + #'credit_amount', + ], + 'links' => [ + @pkgnum_null, '', '', + @post_desc_null, + @peritem_null, + @currency_null, $ilink, $ilink, + $pay_link, + $credit_link, ( map { $_ ne 'Cust. Status' ? $clink : '' } FS::UI::Web::cust_header() ), ], - 'align' => 'rlrrrc'.FS::UI::Web::cust_aligns(), + #'align' => 'rlrrrc'.FS::UI::Web::cust_aligns(), + 'align' => $pkgnum_align. + 'rl'. + $post_desc_align. + $peritem_align. + $currency_align. + 'rcrr'. + FS::UI::Web::cust_aligns(), 'color' => [ + @pkgnum_null, '', '', + @post_desc_null, + @peritem_null, + @currency_null, '', '', '', @@ -49,163 +89,645 @@ FS::UI::Web::cust_colors(), ], 'style' => [ + @pkgnum_null, '', '', + @post_desc_null, + @peritem_null, + @currency_null, '', '', '', '', FS::UI::Web::cust_styles(), ], - ) -%> +&> +<%doc> + +Output control parameters: +- distribute: Boolean. If true, recurring fees will be "prorated" for the + portion of the package date range (sdate-edate) that falls within the date + range of the report. Line items will be limited to those for which this + portion is > 0. This disables filtering on invoice date. + +- usage: Separate usage (cust_bill_pkg_detail records) from + recurring charges. If set to "usage", will show usage instead of + recurring charges. If set to "recurring", will deduct usage and only + show the flat rate charge. If not passed, the "recurring charge" column + will include usage charges also. + +Filtering parameters: +- begin, end: Date range. Applies to invoice date, not necessarily package + date range. But see "distribute". + +- status: Customer status (active, suspended, etc.). This will filter on + _current_ customer status, not status at the time the invoice was generated. + +- agentnum: Filter on customer agent. + +- refnum: Filter on customer reference source. + +- cust_classnum: Filter on customer class. + +- classnum: Filter on package class. + +- report_optionnum: Filter on package report class. Can be a single report + class number or a comma-separated list (where 0 is "no report class"), or the + word "multiple". + +- use_override: Apply "classnum" and "taxclass" filtering based on the + override (bundle) pkgpart, rather than always using the true pkgpart. + +- nottax: Limit to items that are not taxes (pkgnum > 0). + +- istax: Limit to items that are taxes (pkgnum == 0). + +- 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 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; + if "istax" the location of the tax. + +- taxname, taxnameNULL: With "nottax", limit to items whose tax location + matches a tax with this name. With "istax", limit to items that have + this tax name. taxnameNULL is equivalent to "taxname = '' OR taxname + = 'Tax'". + +- out: With "nottax", limit to items that don't match any tax definition. + With "istax", find tax items that are unlinked to their tax definitions. + Current Freeside (> July 2012) always creates tax links, but unlinked + items may result from an incomplete upgrade of legacy data. + +- locationtaxid: With "nottax", limit to packages matching this + tax_rate_location ID; with "tax", limit to taxes generated from that + location. + +- taxclass: Filter on package taxclass. + +- taxclassNULL: With "nottax", limit to items that would be subject to the + tax with taxclass = NULL. This doesn't necessarily mean part_pkg.taxclass + is NULL; it also includes taxclasses that don't have a tax in this region. + +- itemdesc: Limit to line items with this description. Note that non-tax + packages usually have a description of NULL. (Deprecated.) + +- report_group: Can contain '=' or '!=' followed by a string to limit to + line items where itemdesc starts with, or doesn't start with, the string. + +- cust_tax: Limit to customers who are tax-exempt. If "taxname" is also + specified, limit to customers who are also specifically exempt from that + tax. + +- pkg_tax: Limit to packages that are tax-exempt, and only include the + exempt portion (setup, recurring, or both) when calculating totals. + +- taxable: Limit to packages that are subject to tax, i.e. where a + cust_bill_pkg_tax_location record exists. + +- credit: Limit to line items that received a credit application. The + amount of the credit will also be shown. + + <%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'); -#here is the agent virtualization +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +my @select = ( 'cust_bill_pkg.*', 'cust_bill._date' ); +my @total = ( 'COUNT(*)', 'SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)'); +my @total_desc = ( $money_char.'%.2f total' ); # sprintf strings + +my @peritem = ( 'setup', 'recur' ); +my @peritem_desc = ( 'Setup charge', 'Recurring charge' ); + +my @currency_desc = (); +my @currency_sub = (); +my @currency = (); +if ( $conf->config('currencies') ) { + @currency_desc = ( 'Setup billed', 'Recurring billed' ); + @currency_sub = ( + map { + my $what = $_; + sub { my $currency = $_[0]->get($what.'_billed_currency') or return ''; + $currency. ' '. currency_symbol($currency, SYM_HTML). + $_[0]->get($what.'_billed_amount'); + }; + } qw( setup recur ) + ); + @currency = ( 'setup_billed_amount', 'recur_billed_amount' ); #for sorting +} + +my @pkgnum_header = (); +my @pkgnum = (); +my @pkgnum_null; +my $pkgnum_align = ''; +if ( $curuser->option('show_pkgnum') ) { + push @select, 'cust_bill_pkg.pkgnum'; + push @pkgnum_header, 'Pkg Num'; + push @pkgnum, sub { $_[0]->pkgnum > 0 ? $_[0]->pkgnum : '' }; + push @pkgnum_null, ''; + $pkgnum_align .= 'r'; +} + +my @post_desc_header = (); +my @post_desc = (); +my @post_desc_null = (); +my $post_desc_align = ''; +if ( $conf->exists('enable_taxclasses') ) { + push @post_desc_header, 'Tax class'; + push @post_desc, 'taxclass'; + push @post_desc_null, ''; + $post_desc_align .= 'l'; + push @select, 'part_pkg.taxclass'; # or should this use override? +} + +# valid in both the tax and non-tax cases +my $join_cust = + " LEFT JOIN cust_bill ON (cust_bill_pkg.invnum = cust_bill.invnum)". + # use cust_pkg.locationnum if it exists + FS::UI::Web::join_cust_main('cust_bill', 'cust_pkg'); + +#agent virtualization my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql( 'table' => 'cust_main' ); my @where = ( $agentnums_sql ); +# date range my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); -push @where, "_date >= $beginning", - "_date <= $ending"; -push @where , " payby != 'COMP' " - unless $cgi->param('include_comp_cust'); +if ( $cgi->param('distribute') == 1 ) { + push @where, "sdate <= $ending", + "edate > $beginning", + ; +} +else { + push @where, "cust_bill._date >= $beginning", + "cust_bill._date <= $ending"; +} + +# status +if ( $cgi->param('status') =~ /^([a-z]+)$/ ) { + push @where, FS::cust_main->cust_status_sql . " = '$1'"; +} +# agentnum if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { push @where, "cust_main.agentnum = $1"; } -if ( $cgi->param('classnum') =~ /^(\d+)$/ ) { - if ( $1 == 0 ) { - push @where, "classnum IS NULL"; - } else { - push @where, "classnum = $1"; - } +# salesnum--see below +# refnum +if ( $cgi->param('refnum') =~ /^(\d+)$/ ) { + push @where, "cust_main.refnum = $1"; } -if ( $cgi->param('out') ) { - - push @where, " - 0 = ( - SELECT COUNT(*) FROM cust_main_county - WHERE ( cust_main_county.county = cust_main.county - OR ( cust_main_county.county IS NULL AND cust_main.county = '' ) - OR ( cust_main_county.county = '' AND cust_main.county IS NULL) - OR ( cust_main_county.county IS NULL AND cust_main.county IS NULL) - ) - AND ( cust_main_county.state = cust_main.state - OR ( cust_main_county.state IS NULL AND cust_main.state = '' ) - OR ( cust_main_county.state = '' AND cust_main.state IS NULL ) - OR ( cust_main_county.state IS NULL AND cust_main.state IS NULL ) - ) - AND cust_main_county.country = cust_main.country - AND cust_main_county.tax > 0 - ) - "; +# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql) +if ( grep { $_ eq 'cust_classnum' } $cgi->param ) { + my @classnums = grep /^\d*$/, $cgi->param('cust_classnum'); + push @where, 'COALESCE( cust_main.classnum, 0) IN ( '. + join(',', map { $_ || '0' } @classnums ). + ' )' + if @classnums; +} -} elsif ( $cgi->param('country' ) ) { - my $county = dbh->quote( $cgi->param('county') ); - my $state = dbh->quote( $cgi->param('state') ); - my $country = dbh->quote( $cgi->param('country') ); - push @where, - " ( county = $county OR $county = '' ) ", - " ( state = $state OR $state = '' ) ", - " country = $country " - ; - push @where, ' taxclass = '. dbh->quote( $cgi->param('taxclass') ) - if $cgi->param('taxclass'); +# custnum +if ( $cgi->param('custnum') =~ /^(\d+)$/ ) { + push @where, "cust_main.custnum = $1"; +} + +# we want the package and its definition if available +my $join_pkg = +' LEFT JOIN cust_pkg USING (pkgnum) + LEFT JOIN part_pkg USING (pkgpart)'; + +my $part_pkg = 'part_pkg'; +if ( $cgi->param('use_override') ) { #"Separate sub-packages from parents" + # still need the real part_pkg for tax applicability, + # so alias this one + $join_pkg .= " LEFT JOIN part_pkg AS override ON ( + COALESCE(cust_bill_pkg.pkgpart_override, cust_pkg.pkgpart, 0) = override.pkgpart + )"; + $part_pkg = 'override'; +} +push @select, 'part_pkg.pkgpart', 'part_pkg.pkg'; # or should this use override? + +# the non-tax case +if ( $cgi->param('nottax') ) { + + push @where, 'cust_bill_pkg.pkgnum > 0'; + + my @tax_where; # will go into a subquery + my @exempt_where; # will also go into a subquery + + # classnum (of override pkgpart if applicable) + # not specified: all classes + # 0: empty class + # N: classnum + if ( grep { $_ eq 'classnum' } $cgi->param ) { + my @classnums = grep /^\d*$/, $cgi->param('classnum'); + push @where, "COALESCE($part_pkg.classnum, 0) IN ( ". + join(',', @classnums ). + ' )' + if @classnums; + } + + if ( grep { $_ eq 'report_optionnum' } $cgi->param ) { + my @nums = grep /^\w+$/, $cgi->param('report_optionnum'); + my $num = join(',', @nums); + push @where, # code reuse FTW + FS::Report::Table->with_report_option( $num, $cgi->param('use_override')); + } + + if ( $cgi->param('report_optionnum') =~ /^(\w+)$/ ) { + ; + } + # taxclass if ( $cgi->param('taxclassNULL') ) { + # a little different from 'taxclass' in that it applies to the + # effective taxclass, not the real one + push @tax_where, 'cust_main_county.taxclass IS NULL' + } elsif ( $cgi->param('taxclass') ) { + push @tax_where, "$part_pkg.taxclass IN (" . + join(', ', map {dbh->quote($_)} $cgi->param('taxclass') ). + ')'; + } + + if ( $cgi->param('exempt_cust') eq 'Y' ) { + # tax-exempt customers + push @exempt_where, "(exempt_cust = 'Y' OR exempt_cust_taxname = 'Y')"; + + } elsif ( $cgi->param('exempt_pkg') eq 'Y' ) { # non-taxable package + # non-taxable package charges + push @exempt_where, "(exempt_setup = 'Y' OR exempt_recur = 'Y')"; + } + # we don't handle exempt_monthly here + + if ( $cgi->param('taxname') ) { # specific taxname + push @tax_where, "COALESCE(cust_main_county.taxname, 'Tax') = ". + dbh->quote($cgi->param('taxname')); + } - my %hash = ( 'country' => scalar($cgi->param('country')) ); - foreach (qw( state county )) { - $hash{$_} = scalar($cgi->param($_)) if $cgi->param($_); + # country:state:county:city:district (may be repeated) + # You can also pass a big list of taxnums but that leads to huge URLs. + # Note that this means "packages whose tax is in this region", not + # "packages in this region". It's meant for links from the tax report. + if ( $cgi->param('region') ) { + my @orwhere; + foreach ( $cgi->param('region') ) { + my %loc; + @loc{qw(country state county city district)} = + split(':', $cgi->param('region')); + my $string = join(' AND ', + map { + if ( $loc{$_} ) { + "$_ = ".dbh->quote($loc{$_}); + } else { + "$_ IS NULL"; + } + } keys(%loc) + ); + push @orwhere, "($string)"; } - my $cust_main_county = qsearchs('cust_main_county', \%hash); - die "unknown base region for empty taxclass" unless $cust_main_county; + push @tax_where, '(' . join(' OR ', @orwhere) . ')' if @orwhere; + } + + # specific taxnums + 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 + # 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. + + my $exempt_sub; - my $same_sql = $cust_main_county->sql_taxclass_sameregion; - push @where, $same_sql if $same_sql; + if ( @exempt_where or @tax_where + or $cgi->param('taxable') or $cgi->param('out') ) + { + # 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) + if (@tax_where or @exempt_where); + + $exempt_sub .= ' GROUP BY billpkgnum'; + + $join_pkg .= " LEFT JOIN ($exempt_sub) AS item_exempt + USING (billpkgnum)"; } + + # 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 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); + + # now do something with that + if ( @exempt_where ) { + + 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 ( $cgi->param('taxable') ) { + + my $taxable = 'cust_bill_pkg.setup + cust_bill_pkg.recur '. + '- COALESCE(item_exempt.exempt_amount, 0)'; + + 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 ( @tax_where ) { + + # union of taxable + all exempt_ cases + push @where, "(EXISTS($tax_sub) OR item_exempt.billpkgnum IS NOT NULL)"; + + } + + # recur/usage separation + if ( $cgi->param('usage') eq 'recurring' ) { + + my $recur_no_usage = FS::cust_bill_pkg->charged_sql('', '', no_usage => 1); + push @select, "($recur_no_usage) AS recur_no_usage"; + $peritem[1] = 'recur_no_usage'; + $total[1] = "SUM(cust_bill_pkg.setup + $recur_no_usage)"; + $total_desc[0] .= ' (excluding usage)'; + + } elsif ( $cgi->param('usage') eq 'usage' ) { + + 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'; + $total[1] = "SUM($usage)"; + $total_desc[0] .= ' usage charges'; + } + +} elsif ( $cgi->param('istax') ) { + + @peritem = ( 'setup' ); # taxes only have setup + @peritem_desc = ( 'Tax charge' ); + + push @where, 'cust_bill_pkg.pkgnum = 0'; + + # tax location when using tax_rate_location + if ( scalar( grep( /locationtaxid/, $cgi->param ) ) ) { + + $join_pkg .= ' LEFT JOIN cust_bill_pkg_tax_rate_location USING ( billpkgnum ) '. + ' LEFT JOIN tax_rate_location USING ( taxratelocationnum )'; + push @where, FS::tax_rate_location->location_sql( + map { $_ => (scalar($cgi->param($_)) || '') } + qw( district city county state locationtaxid ) + ); + + $total[1] = 'SUM( + COALESCE(cust_bill_pkg_tax_rate_location.amount, + cust_bill_pkg.setup + cust_bill_pkg.recur) + )'; + + } elsif ( $cgi->param('out') ) { + + $join_pkg .= ' + LEFT JOIN cust_bill_pkg_tax_location USING (billpkgnum) + '; + push @where, 'cust_bill_pkg_tax_location.billpkgnum IS NULL'; + + # each billpkgnum should appear only once + $total[0] = 'COUNT(*)'; + $total[1] = 'SUM(cust_bill_pkg.setup)'; + + } else { # not locationtaxid or 'out'--the normal case + + $join_pkg .= ' + LEFT JOIN cust_bill_pkg_tax_location USING (billpkgnum) + JOIN cust_main_county USING (taxnum) + '; + + # 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 + if ( $cgi->param('taxclassNULL') ) { + push @where, 'cust_main_county.taxclass IS NULL'; + } + + # taxname + if ( $cgi->param('taxnameNULL') ) { + push @where, 'cust_main_county.taxname IS NULL OR '. + 'cust_main_county.taxname = \'Tax\''; + } elsif ( $cgi->param('taxname') ) { + push @where, 'cust_main_county.taxname = '. + dbh->quote($cgi->param('taxname')); + } + + # specific taxnums + if ( $cgi->param('taxnum') =~ /^([\d,]+)$/) { + push @where, "cust_main_county.taxnum IN ($1)"; + } + + # itemdesc, for some reason + 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')); + } + } + +} # nottax / istax + + +#total payments +my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount) + FROM cust_bill_pay_pkg + WHERE cust_bill_pkg.billpkgnum = cust_bill_pay_pkg.billpkgnum + "; +push @select, "($pay_sub) AS pay_amount"; + + +# credit +if ( $cgi->param('credit') ) { + + my $credit_sub; + + if ( $cgi->param('istax') ) { + # then we need to group/join by billpkgtaxlocationnum, to get only the + # relevant part of partial taxes + my $credit_sub = "SELECT SUM(cust_credit_bill_pkg.amount) AS credit_amount, + reason.reason as reason_text, access_user.username AS username_text, + billpkgtaxlocationnum, billpkgnum + FROM cust_credit_bill_pkg + JOIN cust_credit_bill USING (creditbillnum) + JOIN cust_credit USING (crednum) + LEFT JOIN reason USING (reasonnum) + LEFT JOIN access_user USING (usernum) + GROUP BY billpkgnum, billpkgtaxlocationnum, reason.reason, + access_user.username"; -push @where, 'pkgnum != 0' if $cgi->param('nottax'); -push @where, 'pkgnum = 0' if $cgi->param('istax'); - -push @where, " tax = 'Y' " if $cgi->param('cust_tax'); - -my $count_query; -if ( $cgi->param('pkg_tax') ) { - - $count_query = - "SELECT COUNT(*), SUM( - ( CASE WHEN part_pkg.setuptax = 'Y' - THEN cust_bill_pkg.setup - ELSE 0 - END - ) - + - ( CASE WHEN part_pkg.recurtax = 'Y' - THEN cust_bill_pkg.recur - ELSE 0 - END - ) - ) - "; - - push @where, "( ( part_pkg.setuptax = 'Y' AND cust_bill_pkg.setup > 0 ) - OR ( part_pkg.recurtax = 'Y' AND cust_bill_pkg.recur > 0 ) )", - "( tax != 'Y' OR tax IS NULL )"; + if ( $cgi->param('out') ) { + + # find credits that are applied to the line items, but not to + # a cust_bill_pkg_tax_location link + $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit + USING (billpkgnum)"; + push @where, 'item_credit.billpkgtaxlocationnum IS NULL'; + + } else { + + # find credits that are applied to the CBPTL links that are + # considered "interesting" by the report criteria + $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit + USING (billpkgtaxlocationnum)"; + + } + + } else { + # then only group by billpkgnum + my $credit_sub = "SELECT SUM(cust_credit_bill_pkg.amount) AS credit_amount, + reason.reason as reason_text, access_user.username AS username_text, + billpkgnum + FROM cust_credit_bill_pkg + JOIN cust_credit_bill USING (creditbillnum) + JOIN cust_credit USING (crednum) + LEFT JOIN reason USING (reasonnum) + LEFT JOIN access_user USING (usernum) + GROUP BY billpkgnum, reason.reason, access_user.username"; + $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit USING (billpkgnum)"; + } + + push @where, 'item_credit.billpkgnum IS NOT NULL'; + push @select, 'item_credit.credit_amount', + 'item_credit.username_text', + 'item_credit.reason_text'; + push @peritem, 'credit_amount', 'username_text', 'reason_text'; + push @peritem_desc, 'Credited', 'By', 'Reason'; + push @total, 'SUM(credit_amount)'; + push @total_desc, "$money_char%.2f credited"; } else { - $count_query = - "SELECT COUNT(*), SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)"; + #still want a credit total column + + my $credit_sub = " + SELECT SUM(cust_credit_bill_pkg.amount) + FROM cust_credit_bill_pkg + WHERE cust_bill_pkg.billpkgnum = cust_credit_bill_pkg.billpkgnum + "; + push @select, "($credit_sub) AS credit_amount"; } -my $where = ' WHERE '. join(' AND ', @where); +push @select, 'cust_main.custnum', FS::UI::Web::cust_sql_fields(); + +#salesnum +if ( $cgi->param('salesnum') =~ /^(\d+)$/ ) { -my $join_cust = " - JOIN cust_bill USING ( invnum ) - LEFT JOIN cust_main USING ( custnum ) -"; + 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 $join_pkg = " - LEFT JOIN cust_pkg USING ( pkgnum ) - LEFT JOIN part_pkg USING ( pkgpart ) -"; -$count_query .= " FROM cust_bill_pkg $join_cust $join_pkg $where"; +my $where = join(' AND ', @where); +$where &&= "WHERE $where"; my $query = { 'table' => 'cust_bill_pkg', - 'addl_from' => "$join_cust $join_pkg", + 'addl_from' => "$join_pkg $join_cust", 'hashref' => {}, - 'select' => join(', ', - 'cust_bill_pkg.*', - 'cust_bill._date', - 'part_pkg.pkg', - 'cust_main.custnum', - FS::UI::Web::cust_sql_fields(), - ), + 'select' => join(",\n", @select ), 'extra_sql' => $where, - 'order_by' => 'ORDER BY _date, billpkgnum', + 'order_by' => 'ORDER BY cust_bill._date, cust_bill_pkg.billpkgnum', }; +my $count_query = + 'SELECT ' . join(',', @total) . + " FROM cust_bill_pkg $join_pkg $join_cust + $where"; + +@peritem_desc = map {emt($_)} @peritem_desc; +my @peritem_sub = map { + my $field = $_; + if ($field =~ /_text$/) { # kludge for credit reason/username fields + sub {$_[0]->get($field)}; + } else { + sub { sprintf($money_char.'%.2f', $_[0]->get($field)) } + } +} @peritem; +my @peritem_null = map { '' } @peritem; # placeholders +my $peritem_align = 'r' x scalar(@peritem); + +@currency_desc = map {emt($_)} @currency_desc; +my @currency_null = map { '' } @currency; # placeholders +my $currency_align = 'r' x scalar(@currency); + my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ]; my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ]; -my $conf = new FS::Conf; -my $money_char = $conf->config('money_char') || '$'; +my $pay_link = ''; #[, 'billpkgnum', ]; +my $credit_link = [ "${p}search/cust_credit_bill_pkg.html?billpkgnum=", 'billpkgnum', ]; + +warn "\n\nQUERY:\n".Dumper($query)."\n\nCOUNT_QUERY:\n$count_query\n\n" + if $cgi->param('debug');