bd0e9e33cd82e2935365349b8967fd90a1ce191f
[freeside.git] / httemplate / search / elements / cust_pay_or_refund.html
1 <%doc>
2
3 Examples:
4
5   include( 'elements/cust_pay_or_refund.html',
6                'thing'          => 'pay',
7                'amount_field'   => 'paid',
8                'name_singular'  => 'payment',
9                'name_verb'      => 'paid',
10          )
11
12   include( 'elements/cust_pay_or_refund.html',
13                'thing'          => 'refund',
14                'amount_field'   => 'refund',
15                'name_singular'  => 'refund',
16                'name_verb'      => 'refunded',
17          )
18
19   include( 'elements/cust_pay_or_refund.html',
20                'thing'          => 'pay_pending',
21                'amount_field'   => 'paid',
22                'name_singular'  => 'pending payment',
23                'name_verb'      => 'pending',
24                'disable_link'   => 1,
25                'disable_by'     => 1,
26                'html_init'      => '',
27                'addl_header'    => [],
28                'addl_fields'    => [],
29                'addl_sort_fields' => [],
30                'redirect_empty' => $redirect_empty,
31           )
32
33   include( 'elements/cust_pay_or_refund.html',
34                'table'          => 'h_cust_pay',
35                'amount_field'   => 'paid',
36                'name_singular'  => 'payment',
37                'name_verb'      => 'paid',
38                'pre_header'     => [ 'Transaction',    'By' ],
39                'pre_fields'     => [ 'history_action', 'history_user' ],
40          )
41
42 </%doc>
43 <& grouped-search.html,
44                 'title'          => $title, # XXX: translate
45                 'name_singular'  => $name_singular,
46                 'query'          => $sql_query,
47                 'count_query'    => $count_query,
48                 'count_addl'     => \@count_addl,
49                 'redirect_empty' => $opt{'redirect_empty'},
50                 'header'         => \@header,
51                 'fields'         => \@fields,
52                 'sort_fields'    => \@sort_fields,
53                 'align'          => $align,
54                 'links'          => \@links,
55                 'link_onclicks'  => \@link_onclicks,
56                 'color'          => \@color,
57                 'style'          => \@style,
58
59                 'group_column'   => 'payby',
60                 'group_label'    => 'payby_name',
61                 'amount_field'   => $amount_field,
62                 'subtotal'       => { $opt{amount_field} => "sum($amount_field)" },
63                 'subtotal_row'   => [ 'Subtotal',
64                                       sub { sprintf($money, $_[0]->$amount_field) },
65                                     ],
66                 'total_row'      => [ '<B>Total</B>',
67                                       sub { sprintf("<B>$money</B>", $_[0]->$amount_field) },
68                                     ],
69                 'show_combined'  => 1,
70 &>
71 <%shared>
72 # canonicalize the payby subtype string to an SQL-quoted list
73 my %cardtype_of = (
74   'VisaMC'    => q['VISA card', 'MasterCard'],
75   'Amex'      => q['American Express card'],
76   'Discover'  => q['Discover card'],
77   'Maestro'   => q['Switch', 'Solo', 'Laser'],
78   'Tokenized' => q['Tokenized'],
79 );  
80 </%shared>
81 <%init>
82
83 my %opt = @_;
84
85 my $curuser = $FS::CurrentUser::CurrentUser;
86
87 my $conf = FS::Conf->new;
88 my $money = ($conf->config('money_char') || '$') . '%.2f';
89
90 die "access denied"
91   unless $curuser->access_right('Basic payment and refund reports');
92
93 my $table = $opt{'table'} || 'cust_'.$opt{'thing'};
94
95 my $has_reason = dbdef->table($table)->column('reasonnum') ? 1 : 0;
96
97 my $amount_field = $opt{'amount_field'};
98 my $name_singular = $opt{'name_singular'};
99
100 my $unapplied = $cgi->param('unapplied');
101 my $title = '';
102 $title = 'Unapplied ' if $unapplied;
103 $title .= "\u$name_singular Search Results";
104
105 ###NOT USED???
106 #my $link = '';
107 #if (    ( $curuser->access_right('View invoices') #remove in 2.5 (2.7?)
108 #          || ($curuser->access_right('View payments') && $table =~ /^cust_pay/)
109 #          || ($curuser->access_right('View refunds') && $table eq 'cust_refund')
110 #        )
111 #     && ! $opt{'disable_link'}
112 #   )
113 #{
114 #
115 #  my $key;
116 #  my $q = '';
117 #  if ( $table eq 'cust_pay_void' ) {
118 #    $key = 'paynum';
119 #    $q .= 'void=1;';
120 #  } elsif ( $table eq /^cust_(\w+)$/ ) {
121 #    $key = $1.'num';
122 #  }
123 #  
124 #  if ( $key ) {
125 #    $q .= "$key=";
126 #    $link = [ "${p}view/$table.html?$q", $key ]
127 #  }
128 #}
129
130 my $cust_link = sub {
131   my $cust_thing = shift;
132   $cust_thing->cust_main_custnum
133     ? [ "${p}view/cust_main.cgi?", 'custnum' ] 
134     : '';
135 };
136
137 # only valid for $table == 'cust_pay' atm
138 my  $tax_names = '';
139 if ( $cgi->param('tax_names') ) {
140   if ( dbh->{Driver}->{Name} =~ /^Pg/i ) {
141
142     $tax_names = "
143       array_to_string(
144         array(
145           SELECT itemdesc
146             FROM cust_bill_pay
147             LEFT JOIN cust_bill_pay_pkg USING ( billpaynum )
148             LEFT JOIN cust_bill_pkg USING ( billpkgnum )
149               WHERE cust_bill_pkg.pkgnum = 0
150                 AND cust_bill_pay.paynum = cust_pay.paynum
151         ), '|'
152       ) AS tax_names"
153     ;
154
155   } elsif ( dbh->{Driver}->{Name} =~ /^mysql/i ) {
156
157     $tax_names = "GROUP_CONCAT(itemdesc SEPARATOR '|') AS tax_names";
158
159   } else {
160
161     warn "warning: unknown database type ". dbh->{Driver}->{Name}.
162          "omitting tax name information from report.";
163
164   }
165 }
166
167 my @header;
168 my @fields;
169 my @sort_fields;
170 my $align = '';
171 my @links;
172 my @link_onclicks;
173 if ( $opt{'pre_header'} ) {
174   push @header, @{ $opt{'pre_header'} };
175   $align .= 'c' x scalar(@{ $opt{'pre_header'} });
176   push @links, map '', @{ $opt{'pre_header'} };
177   push @fields, @{ $opt{'pre_fields'} };
178   push @sort_fields, @{ $opt{'pre_fields'} };
179 }
180
181 my $sub_receipt = $opt{'disable_link'} ? '' : sub {
182   my $obj = shift;
183   my $objnum = $obj->primary_key . '=' . $obj->get($obj->primary_key);
184   my $table = $obj->table;
185   my $void = '';
186   if ($table eq 'cust_pay_void') {
187     $table = 'cust_pay';
188     $void = ';void=1';
189   }
190
191   include('/elements/popup_link_onclick.html',
192     'action'  => $p.'view/'.$table.'.html?link=popup;'.$objnum.$void,
193     'actionlabel' => emt('Payment Receipt'),
194   );
195 };
196
197 push @header, "\u$name_singular",
198               'Amount',
199 ;
200 $align .= 'rr';
201 push @links, '', '';
202 push @fields, 'payby_payinfo_pretty',
203               sub { sprintf($money, shift->$amount_field() ) },
204 ;
205 push @link_onclicks, $sub_receipt, '';
206 push @sort_fields, 'paysort', $amount_field;
207
208 # 4.x, to remain functional while the upgrade is running...
209 my $sub_guess_cardtype = sub {
210   my $row = shift;
211   $row->paycardtype || (
212     ($row->payby eq 'CARD'  && $row->paymask !~ /N\/A/)
213     ? cardtype($row->paymask)
214     : ''
215   )
216 };
217
218 if ($opt{'show_card_type'}) {
219   push @header, emt('Card Type');
220   $align .= 'r';
221   push @links, '';
222   push @fields, $sub_guess_cardtype;
223   # worst case, paycardtype isn't filled in yet and sorting by that column
224   # does nothing.
225   push @sort_fields, 'paycardtype';
226 }
227
228 if ( $unapplied ) {
229   push @header, emt('Unapplied');
230   $align .= 'r';
231   push @links, '';
232   push @fields, sub { sprintf($money, shift->unapplied_amount) };
233   push @sort_fields, '';
234 }
235
236 push @header, emt('Date');
237 $align .= 'r';
238 push @links, '';
239 push @fields, sub { time2str('%b %d %Y', shift->_date ) };
240 push @sort_fields, '_date';
241
242 if ($cgi->param('show_order_number')) {
243   push @header, emt('Order Number');
244   $align .= 'r';
245   push @links, '';
246   push @fields, 'order_number';
247   push @sort_fields, 'order_number';
248 }
249
250 unless ( $opt{'disable_by'} ) {
251   push @header, emt('By');
252   $align .= 'c';
253   push @links, '';
254   push @fields, sub { my $o = shift->otaker;
255                       $o = 'auto billing'          if $o eq 'fs_daily';
256                       $o = 'customer self-service' if $o eq 'fs_selfservice';
257                       $o;
258                     };
259   push @sort_fields, '';
260 }
261
262 if ( $tax_names ) {
263   push @header, (emt('Tax names'), emt('Tax province'));
264   $align .= 'cc';
265   push @links, ('','');
266   push @fields, sub { join (' + ', map { /^(.*?)(, \w\w)?$/; $1 }
267                                    split('\|', shift->tax_names)
268                            );
269                     };
270   push @fields, sub { join (' + ', map { if (/^(?:.*)(?:, )(\w\w)$/){ $1 }
271                                          else { () }
272                                        }
273                                    split('\|', shift->tax_names)
274                            );
275                     };
276   push @sort_fields, '', '';
277 }
278
279 push @header, FS::UI::Web::cust_header();
280 $align .=  FS::UI::Web::cust_aligns();
281 push @links, map { $_ ne 'Cust. Status' ? $cust_link : '' }
282                  FS::UI::Web::cust_header();
283 my @color = ( ( map '', @fields ), FS::UI::Web::cust_colors() );
284 my @style = ( ( map '', @fields ), FS::UI::Web::cust_styles() );
285 push @fields, \&FS::UI::Web::cust_fields;
286 push @sort_fields, FS::UI::Web::cust_sort_fields;
287
288 push @header, @{ $opt{'addl_header'} }
289   if $opt{'addl_header'};
290 push @fields, @{ $opt{'addl_fields'} }
291   if $opt{'addl_fields'};
292 push @sort_fields, @{ $opt{'addl_sort_fields'} }
293   if $opt{'addl_sort_fields'};
294
295 my( $count_query, $sql_query, @count_addl );
296 if ( $cgi->param('magic') ) {
297
298   my @search = ();
299   my @select = (
300     "$table.*",
301     "( $table.payby || ' ' || coalesce($table.paymask, $table.payinfo) ) AS paysort",
302     FS::UI::Web::cust_sql_fields(),
303     'cust_main.custnum AS cust_main_custnum',
304   );
305   push @select, $tax_names if $tax_names;
306
307   my $orderby;
308   if ( $cgi->param('magic') eq '_date' ) {
309
310     if ( $cgi->param('agentnum') && $cgi->param('agentnum') =~ /^(\d+)$/ ) {
311       push @search, "cust_main.agentnum = $1"; # $search{'agentnum'} = $1;
312       my $agent = qsearchs('agent', { 'agentnum' => $1 } );
313       die "unknown agentnum $1" unless $agent;
314       $title = $agent->agent. " $title";
315     }
316
317     if ( $cgi->param('refnum') && $cgi->param('refnum') =~ /^(\d+)$/ ) {
318       push @search, "cust_main.refnum = $1";
319       my $part_referral = qsearchs('part_referral', { 'refnum' => $1 } );
320       die "unknown refnum $1" unless $part_referral;
321       $title = $part_referral->referral. " $title";
322     }
323
324     # cust_classnum - standard matching
325     push @search, $m->comp('match-classnum',
326         param => 'cust_classnum', field => 'cust_main.classnum'
327       );
328
329     if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
330       push @search, "$table.custnum = $1";
331     }
332
333     if ( $cgi->param('payby') ) {
334
335       my @all_payby_search = ();
336       foreach my $payby_string ( $cgi->param('payby') ) {
337
338         my $payby_search;
339
340         my ($payby, $subtype) = split('-', $payby_string);
341         # make sure it exists and is a transaction type
342         if ( FS::payby->payment_payby2longname($payby) ) {
343           $payby_search = "$table.payby = " . dbh->quote($payby);
344         } else {
345           die "illegal payby $payby_string";
346         }
347
348         if ( $subtype ) {
349
350           my $in_cardtype = $cardtype_of{$subtype}
351             or die "unknown card type $subtype";
352           # will complete this phrase after alt_search
353           $payby_search .= " AND ( $table.paycardtype IN($in_cardtype)";
354
355           # 4.x transitional, to avoid breaking things while we upgrade
356           my $similar_to = dbh->{Driver}->{Name} =~ /^mysql/i
357                              ? 'REGEXP' #doesn't behave exactly the same, but
358                                         #should work for our patterns
359                              : 'SIMILAR TO';
360
361           my $alt_search;
362           if ( $subtype eq 'VisaMC' ) {
363
364             #avoid posix regexes for portability
365             $alt_search =
366               # Visa
367               " ( (     substring($table.payinfo from 1 for 1) = '4'     ".
368               #   is not Switch
369               "     AND substring($table.payinfo from 1 for 4) != '4936' ".
370               "     AND substring($table.payinfo from 1 for 6)           ".
371               "         NOT $similar_to '49030[2-9]'                        ".
372               "     AND substring($table.payinfo from 1 for 6)           ".
373               "         NOT $similar_to '49033[5-9]'                        ".
374               "     AND substring($table.payinfo from 1 for 6)           ".
375               "         NOT $similar_to '49110[1-2]'                        ".
376               "     AND substring($table.payinfo from 1 for 6)           ".
377               "         NOT $similar_to '49117[4-9]'                        ".
378               "     AND substring($table.payinfo from 1 for 6)           ".
379               "         NOT $similar_to '49118[1-2]'                        ".
380               "   )".
381               # MasterCard
382               "   OR substring($table.payinfo from 1 for 2) = '51' ".
383               "   OR substring($table.payinfo from 1 for 2) = '52' ".
384               "   OR substring($table.payinfo from 1 for 2) = '53' ".
385               "   OR substring($table.payinfo from 1 for 2) = '54' ".
386               "   OR substring($table.payinfo from 1 for 2) = '54' ".
387               "   OR substring($table.payinfo from 1 for 2) = '55' ".
388               "   OR substring($table.payinfo from 1 for 4) $similar_to '222[1-9]' ".
389               "   OR substring($table.payinfo from 1 for 3) $similar_to '22[3-9]' ".
390               "   OR substring($table.payinfo from 1 for 2) $similar_to '2[3-6]' ".
391               "   OR substring($table.payinfo from 1 for 3) $similar_to '27[0-1]' ".
392               "   OR substring($table.payinfo from 1 for 4) = '2720' ".
393               "   OR substring($table.payinfo from 1 for 3) = '2[2-7]x' ".
394               " ) ";
395
396           } elsif ( $subtype eq 'Amex' ) {
397
398             $alt_search =
399               " (    substring($table.payinfo from 1 for 2 ) = '34' ".
400               "   OR substring($table.payinfo from 1 for 2 ) = '37' ".
401               " ) ";
402
403           } elsif ( $subtype eq 'Discover' ) {
404
405             my $country = $conf->config('countrydefault') || 'US';
406
407             $alt_search =
408               " (    substring($table.payinfo from 1 for 4 ) = '6011'  ".
409               "   OR substring($table.payinfo from 1 for 3 ) = '60x'   ".
410               "   OR substring($table.payinfo from 1 for 2 ) = '65'    ".
411
412               # diner's 300-305 / 3095
413               "   OR substring($table.payinfo from 1 for 3 ) = '300'   ".
414               "   OR substring($table.payinfo from 1 for 3 ) = '301'   ".
415               "   OR substring($table.payinfo from 1 for 3 ) = '302'   ".
416               "   OR substring($table.payinfo from 1 for 3 ) = '303'   ".
417               "   OR substring($table.payinfo from 1 for 3 ) = '304'   ".
418               "   OR substring($table.payinfo from 1 for 3 ) = '305'   ".
419               "   OR substring($table.payinfo from 1 for 4 ) = '3095'  ".
420               "   OR substring($table.payinfo from 1 for 3 ) = '30x'   ".
421
422               # diner's 36, 38, 39
423               "   OR substring($table.payinfo from 1 for 2 ) = '36'    ".
424               "   OR substring($table.payinfo from 1 for 2 ) = '38'    ".
425               "   OR substring($table.payinfo from 1 for 2 ) = '39'    ".
426
427               "   OR substring($table.payinfo from 1 for 3 ) = '644'   ".
428               "   OR substring($table.payinfo from 1 for 3 ) = '645'   ".
429               "   OR substring($table.payinfo from 1 for 3 ) = '646'   ".
430               "   OR substring($table.payinfo from 1 for 3 ) = '647'   ".
431               "   OR substring($table.payinfo from 1 for 3 ) = '648'   ".
432               "   OR substring($table.payinfo from 1 for 3 ) = '649'   ".
433               "   OR substring($table.payinfo from 1 for 3 ) = '64x'   ".
434
435               # JCB cards in the 3528-3589 range identified as Discover inside US & territories (NOT Canada)
436               ( $country =~ /^(US|PR|VI|MP|PW|GU)$/
437                ?" OR substring($table.payinfo from 1 for 4 ) = '3528'  ".
438                 " OR substring($table.payinfo from 1 for 4 ) = '3529'  ".
439                 " OR substring($table.payinfo from 1 for 3 ) = '353'   ".
440                 " OR substring($table.payinfo from 1 for 3 ) = '354'   ".
441                 " OR substring($table.payinfo from 1 for 3 ) = '355'   ".
442                 " OR substring($table.payinfo from 1 for 3 ) = '356'   ".
443                 " OR substring($table.payinfo from 1 for 3 ) = '357'   ".
444                 " OR substring($table.payinfo from 1 for 3 ) = '358'   ".
445                 " OR substring($table.payinfo from 1 for 3 ) = '35x'   "
446                :""
447               ).
448
449               #China Union Pay processed as Discover in US, Mexico and Caribbean
450               ( $country =~ /^(US|MX|AI|AG|AW|BS|BB|BM|BQ|VG|KY|CW|DM|DO|GD|GP|JM|MQ|MS|BL|KN|LC|VC|MF|SX|TT|TC)$/
451                ?" OR substring($table.payinfo from 1 for 3 ) $similar_to '62[24-68x]'   "
452                :""
453               ).
454
455               " ) ";
456
457           } elsif ( $subtype eq 'Maestro' ) {
458
459             $alt_search =
460               " (    substring($table.payinfo from 1 for 2 ) = '63'     ".
461               "   OR substring($table.payinfo from 1 for 2 ) = '67'     ".
462               "   OR substring($table.payinfo from 1 for 6 ) = '564182' ".
463               "   OR substring($table.payinfo from 1 for 4 ) = '4936'   ".
464               "   OR substring($table.payinfo from 1 for 6 )            ".
465               "      $similar_to '49030[2-9]'                             ".
466               "   OR substring($table.payinfo from 1 for 6 )            ".
467               "      $similar_to '49033[5-9]'                             ".
468               "   OR substring($table.payinfo from 1 for 6 )            ".
469               "      $similar_to '49110[1-2]'                             ".
470               "   OR substring($table.payinfo from 1 for 6 )            ".
471               "      $similar_to '49117[4-9]'                             ".
472               "   OR substring($table.payinfo from 1 for 6 )            ".
473               "      $similar_to '49118[1-2]'                             ".
474               " ) ";
475
476           } elsif ( $subtype eq 'Tokenized' ) {
477
478               $alt_search = " substring($table.payinfo from 1 for 2 ) = '99' ";
479
480           } else { # shouldn't happen if there's a $subtype
481
482             $alt_search = 'TRUE';
483  
484           }
485
486           # alt_search is already paren'd if it contains OR.
487           # now make sure it works if they're encrypted.
488           my $masksearch = $alt_search;
489           $masksearch =~ s/$table.payinfo/$table.paymask/g;
490           $alt_search = "( ($table.paymask IS NOT NULL AND $masksearch)
491                           OR $alt_search )";
492
493           # close paren here
494           $payby_search .= " OR ( $table.paycardtype IS NULL AND $alt_search ) )";
495
496         } # if $subtype
497
498         push @all_payby_search, $payby_search;
499
500       }
501
502       push @search, ' ( '. join(' OR ', @all_payby_search). ' ) ' if @all_payby_search;
503
504     }
505
506     if ( $cgi->param('paymask') ) {
507       $cgi->param('paymask') =~ /^\s*(\d+)\s*$/
508         or die "illegal paymask ". $cgi->param('paymask');
509       my $regexp = regexp_sql();
510       push @search, "$table.paymask $regexp '$1\$'";
511     } 
512
513     if ( $cgi->param('payinfo') ) {
514       $cgi->param('payinfo') =~ /^\s*(\d+)\s*$/
515         or die "illegal payinfo ". $cgi->param('payinfo');
516       my $regexp = regexp_sql();
517       push @search, "$table.payinfo $regexp '^0*$1\$'";
518     }
519
520     if ( $cgi->param('ccpay') =~ /^([\w-:]+)$/ ) {
521       # I think that's all the characters we need to allow.
522       # To avoid confusion, this parameter searches both auth and order_number.
523       push @search, "($table.auth LIKE '$1%') OR ($table.order_number LIKE '$1%')";
524       push @fields, 'auth', 'order_number';
525       push @header, 'Auth #', 'Transaction #';
526       push @sort_fields, '', '';
527       $align .= 'rr';
528
529     }
530
531     if ( $cgi->param('usernum') =~ /^(\d+)$/ ) {
532       push @search, "$table.usernum = $1";
533     }
534
535     #for cust_pay_pending...  statusNOT=done
536     if ( $cgi->param('statusNOT') =~ /^(\w+)$/ ) {
537       push @search, "$table.status != '$1'";
538     }
539
540     my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
541
542     push @search, "$table._date >= $beginning ",
543                   "$table._date <= $ending";
544
545     if ( $table eq 'cust_pay_void' ) {
546       my($v_beginning, $v_ending) =
547         FS::UI::Web::parse_beginning_ending($cgi, 'void');
548       push @search, "$table.void_date >= $v_beginning ",
549                     "$table.void_date <= $v_ending";
550     }
551
552     push @search, FS::UI::Web::parse_lt_gt($cgi, $amount_field, $table);
553
554     $orderby = '_date';
555
556   } elsif ( $cgi->param('magic') eq 'paybatch' ) {
557
558     $cgi->param('paybatch') =~ /^([\w\/\:\-\.]+)$/
559       or die "illegal paybatch: ". $cgi->param('paybatch');
560
561     $orderby = "LOWER(company || ' ' || last || ' ' || first )";
562
563   } elsif ( $cgi->param('magic') eq 'batchnum' ) {
564
565     $cgi->param('batchnum') =~ /^(\d+)$/
566       or die "illegal batchnum: ".$cgi->param('batchnum');
567
568     push @search, "batchnum = $1";
569
570     $orderby = "LOWER(company || ' ' || last || ' ' || first )";
571
572   } else {
573     die "unknown search magic: ". $cgi->param('magic');
574   }
575
576   if ( $cgi->param('paybatch') =~ /^([\w\/\:\-\.]+)$/ ) {
577     push @search, "paybatch = '$1'";
578   }
579
580   #unapplied payment/refund
581   if ( $unapplied ) {
582     push @select, '(' . "FS::$table"->unapplied_sql . ') AS unapplied_amount';
583     push @search, "FS::$table"->unapplied_sql . ' > 0';
584
585   }
586
587   #for the history search
588   if ( $cgi->param('history_action') =~ /^([\w,]+)$/ ) {
589     my @history_action = split(/,/, $1);
590     push @search, 'history_action IN ('.
591                     join(',', map "'$_'", @history_action ). ')';
592   }
593
594   if (    $cgi->param('history_date_beginning')
595        || $cgi->param('history_date_ending')    ) {
596       my($h_beginning, $h_ending) =
597         FS::UI::Web::parse_beginning_ending($cgi, 'history_date');
598       push @search, "history_date >= $h_beginning ",
599                     "history_date <= $h_ending";
600   }
601
602   #here is the agent virtualization
603   push @search, $curuser->agentnums_sql;
604
605   my $addl_from = FS::UI::Web::join_cust_main($table);
606   my $group_by = '';
607
608   # reasons, for refunds and voided payments
609   if ( $has_reason ) {
610     push @select, "reason.reason";
611     $addl_from .= " LEFT JOIN reason USING (reasonnum)\n";
612     push @fields, 'reason';
613     push @sort_fields, 'reason.reason';
614     push @header, emt('Reason');
615     if ( $cgi->param('reasonnum') =~ /^(\d+)$/ ) {
616       push @search, "COALESCE(reasonnum, 0) = $1";
617     }
618   }
619
620   #check for customer tags
621   my @tags;
622   foreach my $p ($cgi->param) {
623     if ($p =~ /^tagnum(\d+)/ && $1) {
624       $addl_from .= " LEFT JOIN cust_tag ON (cust_tag.custnum = cust_pay.custnum)" unless @tags;
625       push @tags, $1;
626     }
627   }
628   if (@tags) {
629     my $tags = join(',',@tags);
630     push @search, "cust_tag.tagnum in ($tags)";
631   }
632
633   if ( $cgi->param('tax_names') ) {
634     if ( dbh->{Driver}->{Name} =~ /^Pg/i ) {
635
636       0;#twiddle thumbs
637
638     } elsif ( dbh->{Driver}->{Name} =~ /^mysql/i ) {
639
640       $addl_from .= "LEFT JOIN cust_bill_pay USING ( paynum )
641                      LEFT JOIN cust_bill_pay_pkg USING ( billpaynum )
642                      LEFT JOIN cust_bill_pkg USING ( billpkgnum ) AS tax_names";
643       $group_by  .= "GROUP BY $table.*,cust_main_custnum,".
644                     FS::UI::Web::cust_sql_fields();
645       push @search,
646        "( cust_bill_pkg.pkgnum = 0 OR cust_bill_pkg.pkgnum is NULL )";
647
648     } else {
649
650       warn "warning: unknown database type ". dbh->{Driver}->{Name}.
651            "omitting tax name information from report.";
652
653     }
654   }
655
656   #customer location... total false laziness w/cust_main/Search.pm
657
658   my $current = '';
659   unless ( $cgi->param('location_history') ) {
660     $current = '
661       AND (    cust_location.locationnum IN ( cust_main.bill_locationnum,
662                                               cust_main.ship_locationnum
663                                             )
664             OR cust_location.locationnum IN (
665                  SELECT locationnum FROM cust_pkg
666                   WHERE cust_pkg.custnum = cust_main.custnum
667                     AND locationnum IS NOT NULL
668                     AND '. FS::cust_pkg->ncancelled_recurring_sql.'
669                )
670           )';
671   }
672
673
674   ##
675   # address
676   ##
677   if ( $cgi->param('address') ) {
678     my @values = $cgi->param('address');
679     my @orwhere;
680     foreach (grep /\S/, @values) {
681       my $address = dbh->quote('%'. lc($_). '%');
682       push @orwhere,
683         "LOWER(cust_location.address1) LIKE $address",
684         "LOWER(cust_location.address2) LIKE $address";
685     }
686     if (@orwhere) {
687       push @search, "EXISTS(
688         SELECT 1 FROM cust_location 
689         WHERE cust_location.custnum = cust_main.custnum
690           AND (".join(' OR ',@orwhere).")
691           $current
692         )";
693     }
694   }
695
696   ##
697   # city
698   ##
699   if ( $cgi->param('city') =~ /\S/ ) {
700     my $city = dbh->quote($cgi->param('city'));
701     push @search, "EXISTS(
702       SELECT 1 FROM cust_location
703       WHERE cust_location.custnum = cust_main.custnum
704         AND cust_location.city = $city
705         $current
706     )";
707   }
708
709   ##
710   # county
711   ##
712   if ( $cgi->param('county') =~ /\S/ ) {
713     my $county = dbh->quote($cgi->param('county'));
714     push @search, "EXISTS(
715       SELECT 1 FROM cust_location
716       WHERE cust_location.custnum = cust_main.custnum
717         AND cust_location.county = $county
718         $current
719     )";
720   }
721
722   ##
723   # state
724   ##
725   if ( $cgi->param('state') =~ /\S/ ) {
726     my $state = dbh->quote($cgi->param('state'));
727     push @search, "EXISTS(
728       SELECT 1 FROM cust_location
729       WHERE cust_location.custnum = cust_main.custnum
730         AND cust_location.state = $state
731         $current
732     )";
733   }
734
735   ##
736   # zipcode
737   ##
738   if ( $cgi->param('zip') =~ /\S/ ) {
739     my $zip = dbh->quote($cgi->param('zip') . '%');
740     push @search, "EXISTS(
741       SELECT 1 FROM cust_location
742       WHERE cust_location.custnum = cust_main.custnum
743         AND cust_location.zip LIKE $zip
744         $current
745     )";
746   }
747
748   ##
749   # country
750   ##
751   if ( $cgi->param('country') =~ /^(\w\w)$/ ) {
752     my $country = uc($1);
753     push @search, "EXISTS(
754       SELECT 1 FROM cust_location
755       WHERE cust_location.custnum = cust_main.custnum
756         AND cust_location.country = '$country'
757         $current
758     )";
759   }
760
761   #end of false laziness w/cust_main/Search.pm
762
763   my $search = ' WHERE '. join(' AND ', @search);
764
765   $count_query = "SELECT COUNT(*), SUM($table.$amount_field) ";
766   $count_query .= ', SUM(' . "FS::$table"->unapplied_sql . ') ' 
767     if $unapplied;
768   $count_query .= "FROM $table $addl_from".
769                   "$search $group_by";
770
771   @count_addl = ( '$%.2f total '.$opt{name_verb} );
772   push @count_addl, '$%.2f unapplied' if $unapplied;
773
774   $sql_query = {
775     'table'     => $table,
776     'select'    => join(', ', @select),
777     'hashref'   => {},
778     'extra_sql' => "$search $group_by",
779     'order_by'  => "ORDER BY $orderby",
780     'addl_from' => $addl_from,
781   };
782
783 } else {
784
785   #hmm... is this still used?
786   warn "undefined search magic";
787
788   $cgi->param('payinfo') =~ /^\s*(\d+)\s*$/ or die "illegal payinfo";
789   my $payinfo = $1;
790
791   $cgi->param('payby') =~ /^(\w+)$/ or die "illegal payby";
792   my $payby = $1;
793
794   $count_query = "SELECT COUNT(*), SUM($table.$amount_field) FROM $table".
795                  "  WHERE payinfo = '$payinfo' AND payby = '$payby'".
796                  "  AND ". $curuser->agentnums_sql;
797   @count_addl = ( '$%.2f total '.$opt{name_verb} );
798
799   $sql_query = {
800     'table'     => $table,
801     'hashref'   => { 'payinfo' => $payinfo,
802                      'payby'   => $payby    },
803     'extra_sql' => $curuser->agentnums_sql.
804                    " ORDER BY _date",
805   };
806
807 }
808
809 # for consistency
810 $title = join('',map {ucfirst} split(/\b/,$title));
811
812 </%init>