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