optimize line item detail "Credited" column inefficiency sometimes causing load issue...
[freeside.git] / httemplate / search / cust_bill_pkg.cgi
1 <& elements/search.html,
2                  'title'       => emt('Line items'),
3                  'name'        => emt('line items'),
4                  'query'       => $query,
5                  'count_query' => $count_query,
6                  'count_addl'  => \@total_desc,
7                  'header'      => [
8                    @pkgnum_header,
9                    emt('Pkg Def'),
10                    emt('Description'),
11                    @post_desc_header,
12                    @peritem_desc,
13                    emt('Invoice'),
14                    emt('Date'),
15                    emt('Paid'),
16                    emt('Credited'),
17                    FS::UI::Web::cust_header(),
18                  ],
19                  'fields'      => [
20                    @pkgnum,
21                    sub { $_[0]->pkgnum > 0
22                            # possibly use override.pkg but i think this correct
23                            ? $_[0]->get('pkgpart')
24                            : ''
25                        },
26                    sub { $_[0]->pkgnum > 0
27                            # possibly use override.pkg but i think this correct
28                            ? $_[0]->get('pkg')     
29                            : $_[0]->get('itemdesc')
30                        },
31                    @post_desc,
32                    #strikethrough or "N/A ($amount)" or something these when
33                    # they're not applicable to pkg_tax search
34                    @peritem_sub,
35                    'invnum',
36                    sub { time2str('%b %d %Y', shift->_date ) },
37                    sub { sprintf($money_char.'%.2f', shift->get('pay_amount')) },
38                    sub { sprintf($money_char.'%.2f', shift->get('credit_amount')) },
39                    \&FS::UI::Web::cust_fields,
40                  ],
41                  'sort_fields' => [
42                    @pkgnum_null,
43                    '',
44                    '',
45                    @post_desc_null,
46                    @peritem,
47                    'invnum',
48                    '_date',
49                    #'pay_amount',
50                    #'credit_amount',
51                  ],
52                  'links'       => [
53                    @pkgnum_null,
54                    '',
55                    '',
56                    @post_desc_null,
57                    @peritem_null,
58                    $ilink,
59                    $ilink,
60                    $pay_link,
61                    $credit_link,
62                    ( map { $_ ne 'Cust. Status' ? $clink : '' }
63                          FS::UI::Web::cust_header()
64                    ),
65                  ],
66                  #'align' => 'rlrrrc'.FS::UI::Web::cust_aligns(),
67                  'align' => $pkgnum_align.
68                             'rl'.
69                             $post_desc_align.
70                             $peritem_align.
71                             'rcrr'.
72                             FS::UI::Web::cust_aligns(),
73                  'color' => [ 
74                               @pkgnum_null,
75                               '',
76                               '',
77                               @post_desc_null,
78                               @peritem_null,
79                               '',
80                               '',
81                               '',
82                               '',
83                               FS::UI::Web::cust_colors(),
84                             ],
85                  'style' => [ 
86                               @pkgnum_null,
87                               '',
88                               '',
89                               @post_desc_null,
90                               @peritem_null,
91                               '',
92                               '',
93                               '',
94                               '',
95                               FS::UI::Web::cust_styles(),
96                             ],
97 &>
98 <%doc>
99
100 Output control parameters:
101 - distribute: Boolean.  If true, recurring fees will be "prorated" for the 
102   portion of the package date range (sdate-edate) that falls within the date
103   range of the report.  Line items will be limited to those for which this 
104   portion is > 0.  This disables filtering on invoice date.
105
106 - usage: Separate usage (cust_bill_pkg_detail records) from
107   recurring charges.  If set to "usage", will show usage instead of 
108   recurring charges.  If set to "recurring", will deduct usage and only
109   show the flat rate charge.  If not passed, the "recurring charge" column
110   will include usage charges also.
111
112 Filtering parameters:
113 - begin, end: Date range.  Applies to invoice date, not necessarily package
114   date range.  But see "distribute".
115
116 - status: Customer status (active, suspended, etc.).  This will filter on 
117   _current_ customer status, not status at the time the invoice was generated.
118
119 - agentnum: Filter on customer agent.
120
121 - refnum: Filter on customer reference source.
122
123 - cust_classnum: Filter on customer class.
124
125 - classnum: Filter on package class.
126
127 - use_override: Apply "classnum" and "taxclass" filtering based on the 
128   override (bundle) pkgpart, rather than always using the true pkgpart.
129
130 - nottax: Limit to items that are not taxes (pkgnum > 0).
131
132 - istax: Limit to items that are taxes (pkgnum == 0).
133
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.
138
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.
142
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 
146   = 'Tax'".
147
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.
152
153 - locationtaxid: With "nottax", limit to packages matching this 
154   tax_rate_location ID; with "tax", limit to taxes generated from that 
155   location.
156
157 - taxclass: Filter on package taxclass.
158
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.
162
163 - itemdesc: Limit to line items with this description.  Note that non-tax
164   packages usually have a description of NULL.  (Deprecated.)
165
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.
168
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 
171   tax.
172
173 - pkg_tax: Limit to packages that are tax-exempt, and only include the 
174   exempt portion (setup, recurring, or both) when calculating totals.
175
176 - taxable: Limit to packages that are subject to tax, i.e. where a
177   cust_bill_pkg_tax_location record exists.
178
179 - credit: Limit to line items that received a credit application.  The
180   amount of the credit will also be shown.
181
182 </%doc>
183 <%init>
184
185 my $curuser = $FS::CurrentUser::CurrentUser;
186
187 die "access denied" unless $curuser->access_right('Financial reports');
188
189 my $conf = new FS::Conf;
190 my $money_char = $conf->config('money_char') || '$';
191
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
195
196 my @peritem = ( 'setup', 'recur' );
197 my @peritem_desc = ( 'Setup charge', 'Recurring charge' );
198
199 my @pkgnum_header = ();
200 my @pkgnum = ();
201 my @pkgnum_null;
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';
209 }
210
211 my @post_desc_header = ();
212 my @post_desc = ();
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';
220   push @select, 'part_pkg.taxclass'; # or should this use override?
221 }
222
223 # valid in both the tax and non-tax cases
224 my $join_cust = 
225   " LEFT JOIN cust_bill USING (invnum)".
226   # use cust_pkg.locationnum if it exists
227   FS::UI::Web::join_cust_main('cust_bill', 'cust_pkg');
228
229 #agent virtualization
230 my $agentnums_sql =
231   $FS::CurrentUser::CurrentUser->agentnums_sql( 'table' => 'cust_main' );
232
233 my @where = ( $agentnums_sql );
234
235 # date range
236 my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
237
238 if ( $cgi->param('distribute') == 1 ) {
239   push @where, "sdate <= $ending",
240                "edate >  $beginning",
241   ;
242 }
243 else {
244   push @where, "cust_bill._date >= $beginning",
245                "cust_bill._date <= $ending";
246 }
247
248 # status
249 if ( $cgi->param('status') =~ /^([a-z]+)$/ ) {
250   push @where, FS::cust_main->cust_status_sql . " = '$1'";
251 }
252
253 # agentnum
254 if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
255   push @where, "cust_main.agentnum = $1";
256 }
257
258 # refnum
259 if ( $cgi->param('refnum') =~ /^(\d+)$/ ) {
260   push @where, "cust_main.refnum = $1";
261 }
262
263 # cust_classnum
264 if ( $cgi->param('cust_classnum') ) {
265   my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
266   push @where, 'cust_main.classnum IN('.join(',',@classnums).')'
267     if @classnums;
268 }
269
270 # custnum
271 if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
272   push @where, "cust_main.custnum = $1";
273 }
274
275 # we want the package and its definition if available
276 my $join_pkg = 
277 ' LEFT JOIN cust_pkg      USING (pkgnum) 
278   LEFT JOIN part_pkg      USING (pkgpart)';
279
280 my $part_pkg = 'part_pkg';
281 if ( $cgi->param('use_override') ) { #"Separate sub-packages from parents"
282   # still need the real part_pkg for tax applicability, 
283   # so alias this one
284   $join_pkg .= " LEFT JOIN part_pkg AS override ON (
285   COALESCE(cust_bill_pkg.pkgpart_override, cust_pkg.pkgpart, 0) = override.pkgpart
286   )";
287   $part_pkg = 'override';
288 }
289 push @select, 'part_pkg.pkgpart', 'part_pkg.pkg'; # or should this use override?
290
291 # the non-tax case
292 if ( $cgi->param('nottax') ) {
293
294   push @where, 'cust_bill_pkg.pkgnum > 0';
295
296   my @tax_where; # will go into a subquery
297   my @exempt_where; # will also go into a subquery
298
299   # classnum (of override pkgpart if applicable)
300   # not specified: all classes
301   # 0: empty class
302   # N: classnum
303   if ( $cgi->param('classnum') =~ /^(\d+)$/ ) {
304     push @where, "COALESCE($part_pkg.classnum, 0) = $1";
305   }
306
307   # taxclass
308   if ( $cgi->param('taxclassNULL') ) {
309     # a little different from 'taxclass' in that it applies to the
310     # effective taxclass, not the real one
311     push @tax_where, 'cust_main_county.taxclass IS NULL'
312   } elsif ( $cgi->param('taxclass') ) {
313     push @tax_where, "$part_pkg.taxclass IN (" .
314                  join(', ', map {dbh->quote($_)} $cgi->param('taxclass') ).
315                  ')';
316   }
317
318   if ( $cgi->param('exempt_cust') eq 'Y' ) {
319     # tax-exempt customers
320     push @exempt_where, "(exempt_cust = 'Y' OR exempt_cust_taxname = 'Y')";
321
322   } elsif ( $cgi->param('exempt_pkg') eq 'Y' ) { # non-taxable package
323     # non-taxable package charges
324     push @exempt_where, "(exempt_setup = 'Y' OR exempt_recur = 'Y')";
325   }
326   # we don't handle exempt_monthly here
327   
328   if ( $cgi->param('taxname') ) { # specific taxname
329       push @tax_where, 'cust_main_county.taxname = '.
330                         dbh->quote($cgi->param('taxname'));
331   } elsif ( $cgi->param('taxnameNULL') ) {
332       push @tax_where, 'cust_main_county.taxname IS NULL OR '.
333                        'cust_main_county.taxname = \'Tax\'';
334   }
335
336   # country:state:county:city:district (may be repeated)
337   # You can also pass a big list of taxnums but that leads to huge URLs.
338   # Note that this means "packages whose tax is in this region", not 
339   # "packages in this region".  It's meant for links from the tax report.
340   if ( $cgi->param('region') ) {
341     my @orwhere;
342     foreach ( $cgi->param('region') ) {
343       my %loc;
344       @loc{qw(country state county city district)} = 
345         split(':', $cgi->param('region'));
346       my $string = join(' AND ',
347             map { 
348               if ( $loc{$_} ) {
349                 "$_ = ".dbh->quote($loc{$_});
350               } else {
351                 "$_ IS NULL";
352               }
353             } keys(%loc)
354       );
355       push @orwhere, "($string)";
356     }
357     push @tax_where, '(' . join(' OR ', @orwhere) . ')' if @orwhere;
358   }
359
360   # specific taxnums
361   if ( $cgi->param('taxnum') ) {
362     my $taxnum_in = join(',', 
363       grep /^\d+$/, $cgi->param('taxnum')
364     );
365     push @tax_where, "cust_main_county.taxnum IN ($taxnum_in)"
366       if $taxnum_in;
367   }
368
369   # If we're showing exempt items, we need to find those with 
370   # cust_tax_exempt_pkg records matching the selected taxes.
371   # If we're showing taxable items, we need to find those with 
372   # cust_bill_pkg_tax_location records.  We also need to find the 
373   # exemption records so that we can show the taxable amount.
374   # If we're showing all items, we need the union of those.
375   # If we're showing 'out' (items that aren't region/class taxable),
376   # then we need the set of all items minus the union of those.
377
378   my $exempt_sub;
379
380   if ( @exempt_where or @tax_where 
381     or $cgi->param('taxable') or $cgi->param('out') )
382   {
383     # process exemption restrictions, including @tax_where
384     my $exempt_sub = 'SELECT SUM(amount) as exempt_amount, billpkgnum 
385     FROM cust_tax_exempt_pkg JOIN cust_main_county USING (taxnum)';
386
387     $exempt_sub .= ' WHERE '.join(' AND ', @tax_where, @exempt_where)
388       if (@tax_where or @exempt_where);
389
390     $exempt_sub .= ' GROUP BY billpkgnum';
391
392     $join_pkg .= " LEFT JOIN ($exempt_sub) AS item_exempt
393     USING (billpkgnum)";
394   }
395  
396   if ( @tax_where or $cgi->param('taxable') or $cgi->param('out') ) { 
397     # process tax restrictions
398     unshift @tax_where,
399       'cust_main_county.tax > 0';
400
401     my $tax_sub = "SELECT invnum, cust_bill_pkg_tax_location.pkgnum
402     FROM cust_bill_pkg_tax_location
403     JOIN cust_bill_pkg AS tax_item USING (billpkgnum)
404     JOIN cust_main_county USING (taxnum)
405     WHERE ". join(' AND ', @tax_where).
406     " GROUP BY invnum, cust_bill_pkg_tax_location.pkgnum";
407
408     $join_pkg .= " LEFT JOIN ($tax_sub) AS item_tax
409     ON (item_tax.invnum = cust_bill_pkg.invnum AND
410         item_tax.pkgnum = cust_bill_pkg.pkgnum)";
411   }
412
413   # now do something with that
414   if ( @exempt_where ) {
415
416     push @where,    'item_exempt.billpkgnum IS NOT NULL';
417     push @select,   'item_exempt.exempt_amount';
418     push @peritem,  'exempt_amount';
419     push @peritem_desc, 'Exempt';
420     push @total,    'SUM(exempt_amount)';
421     push @total_desc, "$money_char%.2f tax-exempt";
422
423   } elsif ( $cgi->param('taxable') ) {
424
425     my $taxable = 'cust_bill_pkg.setup + cust_bill_pkg.recur '.
426                   '- COALESCE(item_exempt.exempt_amount, 0)';
427
428     push @where,    'item_tax.invnum IS NOT NULL';
429     push @select,   "($taxable) AS taxable_amount";
430     push @peritem,  'taxable_amount';
431     push @peritem_desc, 'Taxable';
432     push @total,    "SUM($taxable)";
433     push @total_desc, "$money_char%.2f taxable";
434
435   } elsif ( $cgi->param('out') ) {
436   
437     push @where,    'item_tax.invnum IS NULL',
438                     'item_exempt.billpkgnum IS NULL';
439
440   } elsif ( @tax_where ) {
441
442     # union of taxable + all exempt_ cases
443     push @where,
444       '(item_tax.invnum IS NOT NULL OR item_exempt.billpkgnum IS NOT NULL)';
445
446   }
447
448   # recur/usage separation
449   if ( $cgi->param('usage') eq 'recurring' ) {
450
451     my $recur_no_usage = FS::cust_bill_pkg->charged_sql('', '', no_usage => 1);
452     push @select, "($recur_no_usage) AS recur_no_usage";
453     $peritem[1] = 'recur_no_usage';
454     $total[1] = "SUM(cust_bill_pkg.setup + $recur_no_usage)";
455     $total_desc[0] .= ' (excluding usage)';
456
457   } elsif ( $cgi->param('usage') eq 'usage' ) {
458
459     my $usage = FS::cust_bill_pkg->usage_sql();
460     push @select, "($usage) AS _usage";
461     # there's already a method named 'usage'
462     $peritem[1] = '_usage';
463     $peritem_desc[1] = 'Usage charge';
464     $total[1] = "SUM($usage)";
465     $total_desc[0] .= ' usage charges';
466   }
467
468 } elsif ( $cgi->param('istax') ) {
469
470   @peritem = ( 'setup' ); # taxes only have setup
471   @peritem_desc = ( 'Tax charge' );
472
473   push @where, 'cust_bill_pkg.pkgnum = 0';
474
475   # tax location when using tax_rate_location
476   if ( scalar( grep( /locationtaxid/, $cgi->param ) ) ) {
477
478     $join_pkg .= ' LEFT JOIN cust_bill_pkg_tax_rate_location USING ( billpkgnum ) '.
479                  ' LEFT JOIN tax_rate_location USING ( taxratelocationnum )';
480     push @where, FS::tax_rate_location->location_sql(
481                    map { $_ => (scalar($cgi->param($_)) || '') }
482                      qw( district city county state locationtaxid )
483                  );
484
485     $total[1] = 'SUM(
486       COALESCE(cust_bill_pkg_tax_rate_location.amount, 
487                cust_bill_pkg.setup + cust_bill_pkg.recur)
488     )';
489
490   } elsif ( $cgi->param('out') ) {
491
492     $join_pkg .= '
493       LEFT JOIN cust_bill_pkg_tax_location USING (billpkgnum)
494     ';
495     push @where, 'cust_bill_pkg_tax_location.billpkgnum IS NULL';
496
497     # each billpkgnum should appear only once
498     $total[0] = 'COUNT(*)';
499     $total[1] = 'SUM(cust_bill_pkg.setup)';
500
501   } else { # not locationtaxid or 'out'--the normal case
502
503     $join_pkg .= '
504       LEFT JOIN cust_bill_pkg_tax_location USING (billpkgnum)
505       JOIN cust_main_county           USING (taxnum)
506     ';
507
508     # don't double-count the components of consolidated taxes
509     $total[0] = 'COUNT(DISTINCT cust_bill_pkg.billpkgnum)';
510     $total[1] = 'SUM(cust_bill_pkg_tax_location.amount)';
511   }
512
513   # taxclass
514   if ( $cgi->param('taxclassNULL') ) {
515     push @where, 'cust_main_county.taxclass IS NULL';
516   }
517
518   # taxname
519   if ( $cgi->param('taxnameNULL') ) {
520     push @where, 'cust_main_county.taxname IS NULL OR '.
521                  'cust_main_county.taxname = \'Tax\'';
522   } elsif ( $cgi->param('taxname') ) {
523     push @where, 'cust_main_county.taxname = '.
524                   dbh->quote($cgi->param('taxname'));
525   }
526
527   # specific taxnums
528   if ( $cgi->param('taxnum') ) {
529     my $taxnum_in = join(',', 
530       grep /^\d+$/, $cgi->param('taxnum')
531     );
532     push @where, "cust_main_county.taxnum IN ($taxnum_in)"
533       if $taxnum_in;
534   }
535
536   # report group (itemdesc)
537   if ( $cgi->param('report_group') =~ /^(=|!=) (.*)$/ ) {
538     my ( $group_op, $group_value ) = ( $1, $2 );
539     if ( $group_op eq '=' ) {
540       #push @where, 'itemdesc LIKE '. dbh->quote($group_value.'%');
541       push @where, 'itemdesc = '. dbh->quote($group_value);
542     } elsif ( $group_op eq '!=' ) {
543       push @where, '( itemdesc != '. dbh->quote($group_value) .' OR itemdesc IS NULL )';
544     } else {
545       die "guru meditation #00de: group_op $group_op\n";
546     }
547   }
548
549   # itemdesc, for some reason
550   if ( $cgi->param('itemdesc') ) {
551     if ( $cgi->param('itemdesc') eq 'Tax' ) {
552       push @where, "(itemdesc='Tax' OR itemdesc is null)";
553     } else {
554       push @where, 'itemdesc='. dbh->quote($cgi->param('itemdesc'));
555     }
556   }
557
558 } # nottax / istax
559
560
561 #total payments
562 my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount)
563                  FROM cust_bill_pay_pkg
564                    WHERE cust_bill_pkg.billpkgnum = cust_bill_pay_pkg.billpkgnum
565               ";
566 push @select, "($pay_sub) AS pay_amount";
567
568
569 # credit
570 if ( $cgi->param('credit') ) {
571
572   my $credit_sub;
573
574   if ( $cgi->param('istax') ) {
575     # then we need to group/join by billpkgtaxlocationnum, to get only the 
576     # relevant part of partial taxes
577     my $credit_sub = "SELECT SUM(cust_credit_bill_pkg.amount) AS credit_amount,
578       reason.reason as reason_text, access_user.username AS username_text,
579       billpkgtaxlocationnum, billpkgnum
580     FROM cust_credit_bill_pkg
581       JOIN cust_credit_bill USING (creditbillnum)
582       JOIN cust_credit USING (crednum)
583       LEFT JOIN reason USING (reasonnum)
584       LEFT JOIN access_user USING (usernum)
585     GROUP BY billpkgnum, billpkgtaxlocationnum, reason.reason, 
586       access_user.username";
587
588     if ( $cgi->param('out') ) {
589
590       # find credits that are applied to the line items, but not to 
591       # a cust_bill_pkg_tax_location link
592       $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit
593         USING (billpkgnum)";
594       push @where, 'item_credit.billpkgtaxlocationnum IS NULL';
595
596     } else {
597
598       # find credits that are applied to the CBPTL links that are 
599       # considered "interesting" by the report criteria
600       $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit
601         USING (billpkgtaxlocationnum)";
602
603     }
604
605   } else {
606     # then only group by billpkgnum
607     my $credit_sub = "SELECT SUM(cust_credit_bill_pkg.amount) AS credit_amount,
608       reason.reason as reason_text, access_user.username AS username_text,
609       billpkgnum
610     FROM cust_credit_bill_pkg
611       JOIN cust_credit_bill USING (creditbillnum)
612       JOIN cust_credit USING (crednum)
613       LEFT JOIN reason USING (reasonnum)
614       LEFT JOIN access_user USING (usernum)
615     GROUP BY billpkgnum, reason.reason, access_user.username";
616     $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit USING (billpkgnum)";
617   }
618
619   push @where,    'item_credit.billpkgnum IS NOT NULL';
620   push @select,   'item_credit.credit_amount',
621                   'item_credit.username_text',
622                   'item_credit.reason_text';
623   push @peritem,  'credit_amount', 'username_text', 'reason_text';
624   push @peritem_desc, 'Credited', 'By', 'Reason';
625   push @total,    'SUM(credit_amount)';
626   push @total_desc, "$money_char%.2f credited";
627
628 } else {
629
630   #still want a credit total column
631
632   my $credit_sub = "
633     SELECT SUM(cust_credit_bill_pkg.amount)
634       FROM cust_credit_bill_pkg
635         WHERE cust_bill_pkg.billpkgnum = cust_credit_bill_pkg.billpkgnum
636   ";
637   push @select, "($credit_sub) AS credit_amount";
638
639 }
640
641 push @select, 'cust_main.custnum', FS::UI::Web::cust_sql_fields();
642
643 my $where = join(' AND ', @where);
644 $where &&= "WHERE $where";
645
646 my $query = {
647   'table'     => 'cust_bill_pkg',
648   'addl_from' => "$join_pkg $join_cust",
649   'hashref'   => {},
650   'select'    => join(",\n", @select ),
651   'extra_sql' => $where,
652   'order_by'  => 'ORDER BY cust_bill._date, cust_bill_pkg.billpkgnum',
653 };
654
655 my $count_query =
656   'SELECT ' . join(',', @total) .
657   " FROM cust_bill_pkg $join_pkg $join_cust
658   $where";
659
660 @peritem_desc = map {emt($_)} @peritem_desc;
661 my @peritem_sub = map {
662   my $field = $_;
663   if ($field =~ /_text$/) { # kludge for credit reason/username fields
664     sub {$_[0]->get($field)};
665   } else {
666     sub { sprintf($money_char.'%.2f', $_[0]->get($field)) }
667   }
668 } @peritem;
669 my @peritem_null = map { '' } @peritem; # placeholders
670 my $peritem_align = 'r' x scalar(@peritem);
671
672 my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ];
673 my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ];
674
675 my $pay_link    = ''; #[, 'billpkgnum', ];
676 my $credit_link = [ "${p}search/cust_credit_bill_pkg.html?billpkgnum=", 'billpkgnum', ];
677
678 warn "\n\nQUERY:\n".Dumper($query)."\n\nCOUNT_QUERY:\n$count_query\n\n"
679   if $cgi->param('debug');
680
681 </%init>