1 <& elements/search.html,
2 'title' => emt('Line items'),
3 'name' => emt('line items'),
5 'count_query' => $count_query,
6 'count_addl' => \@total_desc,
17 FS::UI::Web::cust_header(),
21 sub { $_[0]->pkgnum > 0
22 ? $_[0]->get('pkgpart')
25 'itemdesc', # is part_pkg.pkg if applicable
27 #strikethrough or "N/A ($amount)" or something these when
28 # they're not applicable to pkg_tax search
31 sub { time2str('%b %d %Y', shift->_date ) },
32 sub { sprintf($money_char.'%.2f', shift->get('pay_amount')) },
33 sub { sprintf($money_char.'%.2f', shift->get('credit_amount')) },
34 \&FS::UI::Web::cust_fields,
46 FS::UI::Web::cust_sort_fields(),
58 ( map { $_ ne 'Cust. Status' ? $clink : '' }
59 FS::UI::Web::cust_header()
62 #'align' => 'rlrrrc'.FS::UI::Web::cust_aligns(),
63 'align' => $pkgnum_align.
68 FS::UI::Web::cust_aligns(),
79 FS::UI::Web::cust_colors(),
91 FS::UI::Web::cust_styles(),
96 Output control parameters:
97 - distribute: Boolean. If true, recurring fees will be "prorated" for the
98 portion of the package date range (sdate-edate) that falls within the date
99 range of the report. Line items will be limited to those for which this
100 portion is > 0. This disables filtering on invoice date.
102 - usage: Separate usage (cust_bill_pkg_detail records) from
103 recurring charges. If set to "usage", will show usage instead of
104 recurring charges. If set to "recurring", will deduct usage and only
105 show the flat rate charge. If not passed, the "recurring charge" column
106 will include usage charges also.
108 Filtering parameters:
109 - begin, end: Date range. Applies to invoice date, not necessarily package
110 date range. But see "distribute".
112 - status: Customer status (active, suspended, etc.). This will filter on
113 _current_ customer status, not status at the time the invoice was generated.
115 - agentnum: Filter on customer agent.
117 - refnum: Filter on customer reference source.
119 - cust_classnum: Filter on customer class.
121 - classnum: Filter on package class.
123 - report_optionnum: Filter on package report class. Can be a single report
124 class number or a comma-separated list (where 0 is "no report class"), or the
127 - use_override: Apply "classnum" and "taxclass" filtering based on the
128 override (bundle) pkgpart, rather than always using the true pkgpart.
130 - nottax: Limit to items that are not taxes (pkgnum > 0 or feepart > 0).
132 - istax: Limit to items that are taxes (pkgnum == 0 and feepart = null).
134 - taxnum: Limit to items whose tax definition matches this taxnum.
135 With "nottax" that means items that are subject to that tax;
136 with "istax" it's the tax charges themselves. Can be specified
137 more than once to include multiple taxes.
139 - country, state, county, city: Limit to items whose tax location
140 matches these fields. If "nottax" it's the tax location of the package;
141 if "istax" the location of the tax.
143 - taxname, taxnameNULL: With "nottax", limit to items whose tax location
144 matches a tax with this name. With "istax", limit to items that have
145 this tax name. taxnameNULL is equivalent to "taxname = '' OR taxname
148 - out: With "nottax", limit to items that don't match any tax definition.
149 With "istax", find tax items that are unlinked to their tax definitions.
150 Current Freeside (> July 2012) always creates tax links, but unlinked
151 items may result from an incomplete upgrade of legacy data.
153 - locationtaxid: With "nottax", limit to packages matching this
154 tax_rate_location ID; with "tax", limit to taxes generated from that
157 - taxclass: Filter on package taxclass.
159 - taxclassNULL: With "nottax", limit to items that would be subject to the
160 tax with taxclass = NULL. This doesn't necessarily mean part_pkg.taxclass
161 is NULL; it also includes taxclasses that don't have a tax in this region.
163 - itemdesc: Limit to line items with this description. Note that non-tax
164 packages usually have a description of NULL. (Deprecated.)
166 - report_group: Can contain '=' or '!=' followed by a string to limit to
167 line items where itemdesc starts with, or doesn't start with, the string.
169 - cust_tax: Limit to customers who are tax-exempt. If "taxname" is also
170 specified, limit to customers who are also specifically exempt from that
173 - pkg_tax: Limit to packages that are tax-exempt, and only include the
174 exempt portion (setup, recurring, or both) when calculating totals.
176 - taxable: Limit to packages that are subject to tax, i.e. where a
177 cust_bill_pkg_tax_location record exists.
179 - credit: Limit to line items that received a credit application. The
180 amount of the credit will also be shown.
185 my $curuser = $FS::CurrentUser::CurrentUser;
187 die "access denied" unless $curuser->access_right('Financial reports');
189 my $conf = new FS::Conf;
190 my $money_char = $conf->config('money_char') || '$';
192 my @select = ( 'cust_bill_pkg.*', 'cust_bill._date' );
193 my @total = ( 'COUNT(*)', 'SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)');
194 my @total_desc = ( $money_char.'%.2f total' ); # sprintf strings
196 my @peritem = ( 'setup', 'recur' );
197 my @peritem_desc = ( 'Setup charge', 'Recurring charge' );
199 my @pkgnum_header = ();
202 my $pkgnum_align = '';
203 if ( $curuser->option('show_pkgnum') ) {
204 push @select, 'cust_bill_pkg.pkgnum';
205 push @pkgnum_header, 'Pkg Num';
206 push @pkgnum, sub { $_[0]->pkgnum > 0 ? $_[0]->pkgnum : '' };
207 push @pkgnum_null, '';
208 $pkgnum_align .= 'r';
211 my @post_desc_header = ();
213 my @post_desc_null = ();
214 my $post_desc_align = '';
215 if ( $conf->exists('enable_taxclasses') ) {
216 push @post_desc_header, 'Tax class';
217 push @post_desc, 'taxclass';
218 push @post_desc_null, '';
219 $post_desc_align .= 'l';
222 # used in several places
223 my $itemdesc = 'COALESCE(part_fee.itemdesc, part_pkg.pkg, cust_bill_pkg.itemdesc)';
225 # valid in both the tax and non-tax cases
227 " LEFT JOIN cust_bill ON (cust_bill_pkg.invnum = cust_bill.invnum)".
228 # use cust_pkg.locationnum if it exists
229 FS::UI::Web::join_cust_main('cust_bill', 'cust_pkg');
231 #agent virtualization
233 $FS::CurrentUser::CurrentUser->agentnums_sql( 'table' => 'cust_main' );
235 my @where = ( $agentnums_sql );
238 my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
240 if ( $cgi->param('distribute') == 1 ) {
241 push @where, "sdate <= $ending",
242 "edate > $beginning",
245 push @where, "cust_bill._date >= $beginning",
246 "cust_bill._date <= $ending";
250 if ( $cgi->param('status') =~ /^([a-z]+)$/ ) {
251 push @where, FS::cust_main->cust_status_sql . " = '$1'";
255 if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
256 push @where, "cust_main.agentnum = $1";
259 # salesnum--see below
261 if ( $cgi->param('refnum') =~ /^(\d+)$/ ) {
262 push @where, "cust_main.refnum = $1";
265 # 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)
266 if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
267 my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
268 push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
269 join(',', map { $_ || '0' } @classnums ).
276 if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
277 push @where, "cust_main.custnum = $1";
280 # we want the package and its definition if available
282 ' LEFT JOIN cust_pkg USING (pkgnum)
283 LEFT JOIN part_pkg USING (pkgpart)
284 LEFT JOIN part_fee USING (feepart)';
286 my $part_pkg = 'part_pkg';
287 # "Separate sub-packages from parents"
288 my $use_override = $cgi->param('use_override') ? 1 : 0;
289 if ( $use_override ) {
290 # still need the real part_pkg for tax applicability,
292 $join_pkg .= " LEFT JOIN part_pkg AS override ON (
293 COALESCE(cust_bill_pkg.pkgpart_override, cust_pkg.pkgpart, 0) = override.pkgpart
295 $part_pkg = 'override';
297 push @select, "$part_pkg.pkgpart", "$part_pkg.pkg";
298 push @select, "COALESCE($part_pkg.taxclass, part_fee.taxclass) AS taxclass"
299 if $conf->exists('enable_taxclasses');
302 if ( $cgi->param('nottax') ) {
304 push @select, "($itemdesc) AS itemdesc";
307 '(cust_bill_pkg.pkgnum > 0 OR cust_bill_pkg.feepart IS NOT NULL)';
309 my @tax_where; # will go into a subquery
310 my @exempt_where; # will also go into a subquery
312 # classnum (of override pkgpart if applicable)
313 # not specified: all classes
316 if ( grep { $_ eq 'classnum' } $cgi->param ) {
317 my @classnums = grep /^\d+$/, $cgi->param('classnum');
318 push @where, "COALESCE(part_fee.classnum, $part_pkg.classnum, 0) IN ( ".
319 join(',', @classnums ).
324 if ( grep { $_ eq 'report_optionnum' } $cgi->param ) {
325 my $num = join(',', grep /^[\d,]+$/, $cgi->param('report_optionnum'));
326 my $not_num = join(',', grep /^[\d,]+$/, $cgi->param('not_report_optionnum'));
327 my $all = $cgi->param('all_report_options') ? 1 : 0;
328 push @where, # code reuse FTW
329 FS::Report::Table->with_report_option(
330 report_optionnum => $num,
331 not_report_optionnum => $not_num,
332 use_override => $use_override,
333 all_report_options => $all,
338 if ( $cgi->param('taxclassNULL') ) {
339 # a little different from 'taxclass' in that it applies to the
340 # effective taxclass, not the real one
341 push @tax_where, 'cust_main_county.taxclass IS NULL'
342 } elsif ( $cgi->param('taxclass') ) {
343 push @tax_where, "COALESCE(part_fee.taxclass, $part_pkg.taxclass) IN (" .
344 join(', ', map {dbh->quote($_)} $cgi->param('taxclass') ).
348 if ( $cgi->param('exempt_cust') eq 'Y' ) {
349 # tax-exempt customers
350 push @exempt_where, "(exempt_cust = 'Y' OR exempt_cust_taxname = 'Y')";
352 } elsif ( $cgi->param('exempt_pkg') eq 'Y' ) { # non-taxable package
353 # non-taxable package charges
354 push @exempt_where, "(exempt_setup = 'Y' OR exempt_recur = 'Y')";
356 # we don't handle exempt_monthly here
358 if ( $cgi->param('taxname') ) { # specific taxname
359 push @tax_where, 'cust_main_county.taxname = '.
360 dbh->quote($cgi->param('taxname'));
361 } elsif ( $cgi->param('taxnameNULL') ) {
362 push @tax_where, 'cust_main_county.taxname IS NULL OR '.
363 'cust_main_county.taxname = \'Tax\'';
366 # country:state:county:city:district (may be repeated)
367 # You can also pass a big list of taxnums but that leads to huge URLs.
368 # Note that this means "packages whose tax is in this region", not
369 # "packages in this region". It's meant for links from the tax report.
370 if ( $cgi->param('region') ) {
372 foreach ( $cgi->param('region') ) {
374 @loc{qw(country state county city district)} =
375 split(':', $cgi->param('region'));
376 my $string = join(' AND ',
379 "$_ = ".dbh->quote($loc{$_});
385 push @orwhere, "($string)";
387 push @tax_where, '(' . join(' OR ', @orwhere) . ')' if @orwhere;
391 if ( $cgi->param('taxnum') ) {
392 my $taxnum_in = join(',',
393 grep /^\d+$/, $cgi->param('taxnum')
395 push @tax_where, "cust_main_county.taxnum IN ($taxnum_in)"
399 # If we're showing exempt items, we need to find those with
400 # cust_tax_exempt_pkg records matching the selected taxes.
401 # If we're showing taxable items, we need to find those with
402 # cust_bill_pkg_tax_location records. We also need to find the
403 # exemption records so that we can show the taxable amount.
404 # If we're showing all items, we need the union of those.
405 # If we're showing 'out' (items that aren't region/class taxable),
406 # then we need the set of all items minus the union of those.
410 if ( @exempt_where or @tax_where
411 or $cgi->param('taxable') or $cgi->param('out') )
413 # process exemption restrictions, including @tax_where
414 my $exempt_sub = 'SELECT SUM(amount) as exempt_amount, billpkgnum
415 FROM cust_tax_exempt_pkg JOIN cust_main_county USING (taxnum)';
417 $exempt_sub .= ' WHERE '.join(' AND ', @tax_where, @exempt_where)
418 if (@tax_where or @exempt_where);
420 $exempt_sub .= ' GROUP BY billpkgnum';
422 $join_pkg .= " LEFT JOIN ($exempt_sub) AS item_exempt
426 if ( @tax_where or $cgi->param('taxable') or $cgi->param('out') ) {
427 # process tax restrictions
429 'cust_main_county.tax > 0';
431 my $tax_sub = "SELECT invnum, cust_bill_pkg_tax_location.pkgnum
432 FROM cust_bill_pkg_tax_location
433 JOIN cust_bill_pkg AS tax_item USING (billpkgnum)
434 JOIN cust_main_county USING (taxnum)
435 WHERE ". join(' AND ', @tax_where).
436 " GROUP BY invnum, cust_bill_pkg_tax_location.pkgnum";
438 $join_pkg .= " LEFT JOIN ($tax_sub) AS item_tax
439 ON (item_tax.invnum = cust_bill_pkg.invnum AND
440 item_tax.pkgnum = cust_bill_pkg.pkgnum)";
443 # now do something with that
444 if ( @exempt_where ) {
446 push @where, 'item_exempt.billpkgnum IS NOT NULL';
447 push @select, 'item_exempt.exempt_amount';
448 push @peritem, 'exempt_amount';
449 push @peritem_desc, 'Exempt';
450 push @total, 'SUM(exempt_amount)';
451 push @total_desc, "$money_char%.2f tax-exempt";
453 } elsif ( $cgi->param('taxable') ) {
455 my $taxable = 'cust_bill_pkg.setup + cust_bill_pkg.recur '.
456 '- COALESCE(item_exempt.exempt_amount, 0)';
458 push @where, 'item_tax.invnum IS NOT NULL';
459 push @select, "($taxable) AS taxable_amount";
460 push @peritem, 'taxable_amount';
461 push @peritem_desc, 'Taxable';
462 push @total, "SUM($taxable)";
463 push @total_desc, "$money_char%.2f taxable";
465 } elsif ( $cgi->param('out') ) {
467 push @where, 'item_tax.invnum IS NULL',
468 'item_exempt.billpkgnum IS NULL';
470 } elsif ( @tax_where ) {
472 # union of taxable + all exempt_ cases
474 '(item_tax.invnum IS NOT NULL OR item_exempt.billpkgnum IS NOT NULL)';
478 # recur/usage separation
479 if ( $cgi->param('usage') eq 'recurring' ) {
481 my $recur_no_usage = FS::cust_bill_pkg->charged_sql('', '', no_usage => 1);
482 push @select, "($recur_no_usage) AS recur_no_usage";
483 $peritem[1] = 'recur_no_usage';
484 $total[1] = "SUM(cust_bill_pkg.setup + $recur_no_usage)";
485 $total_desc[0] .= ' (excluding usage)';
487 } elsif ( $cgi->param('usage') eq 'usage' ) {
489 my $usage = FS::cust_bill_pkg->usage_sql();
490 push @select, "($usage) AS _usage";
491 # there's already a method named 'usage'
492 $peritem[1] = '_usage';
493 $peritem_desc[1] = 'Usage charge';
494 $total[1] = "SUM($usage)";
495 $total_desc[0] .= ' usage charges';
498 } elsif ( $cgi->param('istax') ) {
500 @peritem = ( 'setup' ); # taxes only have setup
501 @peritem_desc = ( 'Tax charge' );
503 push @where, 'cust_bill_pkg.pkgnum = 0';
505 # tax location when using tax_rate_location
506 if ( $cgi->param('vendortax') ) {
508 $join_pkg .= ' LEFT JOIN cust_bill_pkg_tax_rate_location USING ( billpkgnum ) '.
509 ' LEFT JOIN tax_rate_location USING ( taxratelocationnum )';
510 foreach (qw( state county city locationtaxid)) {
511 if ( scalar($cgi->param($_)) ) {
512 my $place = dbh->quote( $cgi->param($_) );
513 push @where, "tax_rate_location.$_ = $place";
518 COALESCE(cust_bill_pkg_tax_rate_location.amount,
519 cust_bill_pkg.setup + cust_bill_pkg.recur)
522 } elsif ( $cgi->param('out') ) {
525 LEFT JOIN cust_bill_pkg_tax_location USING (billpkgnum)
527 push @where, 'cust_bill_pkg_tax_location.billpkgnum IS NULL';
529 # each billpkgnum should appear only once
530 $total[0] = 'COUNT(*)';
531 $total[1] = 'SUM(cust_bill_pkg.setup)';
533 } else { # not locationtaxid or 'out'--the normal case
536 LEFT JOIN cust_bill_pkg_tax_location USING (billpkgnum)
537 JOIN cust_main_county USING (taxnum)
540 # don't double-count the components of consolidated taxes
541 $total[0] = 'COUNT(DISTINCT cust_bill_pkg.billpkgnum)';
542 $total[1] = 'SUM(cust_bill_pkg_tax_location.amount)';
546 if ( $cgi->param('taxclassNULL') ) {
547 push @where, 'cust_main_county.taxclass IS NULL';
551 if ( $cgi->param('taxnameNULL') ) {
552 push @where, 'cust_main_county.taxname IS NULL OR '.
553 'cust_main_county.taxname = \'Tax\'';
554 } elsif ( $cgi->param('taxname') ) {
555 push @where, 'cust_main_county.taxname = '.
556 dbh->quote($cgi->param('taxname'));
560 if ( $cgi->param('taxnum') ) {
561 my $taxnum_in = join(',',
562 grep /^\d+$/, $cgi->param('taxnum')
564 push @where, "cust_main_county.taxnum IN ($taxnum_in)"
568 # report group (itemdesc)
569 if ( $cgi->param('report_group') =~ /^(=|!=) (.*)$/ ) {
570 my ( $group_op, $group_value ) = ( $1, $2 );
571 if ( $group_op eq '=' ) {
572 #push @where, 'itemdesc LIKE '. dbh->quote($group_value.'%');
573 push @where, 'itemdesc = '. dbh->quote($group_value);
574 } elsif ( $group_op eq '!=' ) {
575 push @where, '( itemdesc != '. dbh->quote($group_value) .' OR itemdesc IS NULL )';
577 die "guru meditation #00de: group_op $group_op\n";
581 # itemdesc, for breakdown from the vendor tax report
582 if ( $cgi->param('itemdesc') ) {
583 if ( $cgi->param('itemdesc') eq 'Tax' ) {
584 push @where, "($itemdesc = 'Tax' OR $itemdesc is null)";
586 push @where, "$itemdesc = ". dbh->quote($cgi->param('itemdesc'));
594 my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount)
595 FROM cust_bill_pay_pkg
596 WHERE cust_bill_pkg.billpkgnum = cust_bill_pay_pkg.billpkgnum
598 push @select, "($pay_sub) AS pay_amount";
602 if ( $cgi->param('credit') ) {
606 my($cr_begin, $cr_end) = FS::UI::Web::parse_beginning_ending($cgi, 'credit');
607 $credit_where = "WHERE cust_credit_bill._date >= $cr_begin " .
608 "AND cust_credit_bill._date <= $cr_end";
612 if ( $cgi->param('istax') ) {
613 # then we need to group/join by billpkgtaxlocationnum, to get only the
614 # relevant part of partial taxes
615 my $credit_sub = "SELECT SUM(cust_credit_bill_pkg.amount) AS credit_amount,
616 reason.reason as reason_text, access_user.username AS username_text,
617 billpkgtaxlocationnum, billpkgnum
618 FROM cust_credit_bill_pkg
619 JOIN cust_credit_bill USING (creditbillnum)
620 JOIN cust_credit USING (crednum)
621 LEFT JOIN reason USING (reasonnum)
622 LEFT JOIN access_user USING (usernum)
624 GROUP BY billpkgnum, billpkgtaxlocationnum, reason.reason,
625 access_user.username";
627 if ( $cgi->param('out') ) {
629 # find credits that are applied to the line items, but not to
630 # a cust_bill_pkg_tax_location link
631 $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit
633 push @where, 'item_credit.billpkgtaxlocationnum IS NULL';
637 # find credits that are applied to the CBPTL links that are
638 # considered "interesting" by the report criteria
639 $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit
640 USING (billpkgtaxlocationnum)";
645 # then only group by billpkgnum
646 my $credit_sub = "SELECT SUM(cust_credit_bill_pkg.amount) AS credit_amount,
647 reason.reason as reason_text, access_user.username AS username_text,
649 FROM cust_credit_bill_pkg
650 JOIN cust_credit_bill USING (creditbillnum)
651 JOIN cust_credit USING (crednum)
652 LEFT JOIN reason USING (reasonnum)
653 LEFT JOIN access_user USING (usernum)
655 GROUP BY billpkgnum, reason.reason, access_user.username";
656 $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit USING (billpkgnum)";
659 push @where, 'item_credit.billpkgnum IS NOT NULL';
660 push @select, 'item_credit.credit_amount',
661 'item_credit.username_text',
662 'item_credit.reason_text';
663 push @peritem, 'credit_amount', 'username_text', 'reason_text';
664 push @peritem_desc, 'Credited', 'By', 'Reason';
665 push @total, 'SUM(credit_amount)';
666 push @total_desc, "$money_char%.2f credited";
670 #still want a credit total column
673 SELECT SUM(cust_credit_bill_pkg.amount)
674 FROM cust_credit_bill_pkg
675 WHERE cust_bill_pkg.billpkgnum = cust_credit_bill_pkg.billpkgnum
677 push @select, "($credit_sub) AS credit_amount";
681 push @select, 'cust_main.custnum', FS::UI::Web::cust_sql_fields();
684 if ( $cgi->param('salesnum') =~ /^(\d+)$/ ) {
687 my $sales = FS::sales->by_key($salesnum)
688 or die "salesnum $salesnum not found";
690 my $subsearch = $sales->cust_bill_pkg_search('', '',
691 'cust_main_sales' => ($cgi->param('cust_main_sales') ? 1 : 0),
692 'paid' => ($cgi->param('paid') ? 1 : 0),
693 'classnum' => scalar($cgi->param('classnum'))
695 $join_pkg .= " JOIN sales_pkg_class ON ( COALESCE(sales_pkg_class.classnum, 0) = COALESCE( part_fee.classnum, part_pkg.classnum, 0) )";
697 my $extra_sql = $subsearch->{extra_sql};
698 $extra_sql =~ s/^WHERE//;
699 push @where, $extra_sql;
701 $cgi->param('classnum', 0) unless $cgi->param('classnum');
705 my $where = join(' AND ', @where);
706 $where &&= "WHERE $where";
709 'table' => 'cust_bill_pkg',
710 'addl_from' => "$join_pkg $join_cust",
712 'select' => join(",\n", @select ),
713 'extra_sql' => $where,
714 'order_by' => 'ORDER BY cust_bill._date, cust_bill_pkg.billpkgnum',
718 'SELECT ' . join(',', @total) .
719 " FROM cust_bill_pkg $join_pkg $join_cust
722 @peritem_desc = map {emt($_)} @peritem_desc;
723 my @peritem_sub = map {
725 if ($field =~ /_text$/) { # kludge for credit reason/username fields
726 sub {$_[0]->get($field)};
728 sub { sprintf($money_char.'%.2f', $_[0]->get($field)) }
731 my @peritem_null = map { '' } @peritem; # placeholders
732 my $peritem_align = 'r' x scalar(@peritem);
734 my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ];
735 my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ];
737 my $pay_link = ''; #[, 'billpkgnum', ];
738 my $credit_link = [ "${p}search/cust_credit_bill_pkg.html?billpkgnum=", 'billpkgnum', ];
740 warn "\n\nQUERY:\n".Dumper($query)."\n\nCOUNT_QUERY:\n$count_query\n\n"
741 if $cgi->param('debug');