1fc818d358b729a520f717350ee41d99ac027b1d
[freeside.git] / FS / FS / cust_bill / Search.pm
1 package FS::cust_bill::Search;
2
3 use strict;
4 use FS::CurrentUser;
5 use FS::UI::Web;
6 use FS::Record qw( qsearchs dbh );
7 use FS::cust_main;
8 use FS::access_user;
9                                                                                 
10 =item search HASHREF                                                            
11                                                                                 
12 (Class method)                                                                  
13                                                                                 
14 Returns a qsearch hash expression to search for parameters specified in HASHREF.
15 In addition to all parameters accepted by search_sql_where, the following
16 additional parameters valid:
17
18 =over 4                                                                         
19
20 =item newest_percust
21
22 =back
23
24 =cut
25
26 sub search {
27   my( $class, $params ) = @_;
28
29   my( $count_query, $count_addl ) = ( '', '' );
30
31   #some false laziness w/cust_bill::re_X
32
33   $count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'"
34     if $params->{'newest_percust'};
35
36   my $extra_sql = FS::cust_bill->search_sql_where( $params );
37   $extra_sql = "WHERE $extra_sql" if $extra_sql;
38
39   my $join_cust_main = FS::UI::Web::join_cust_main('cust_bill');
40
41   unless ( $count_query ) {
42     $count_query = 'SELECT COUNT(*), '. join(', ',
43                      map "SUM($_)",
44                          ( 'charged',
45                            FS::cust_bill->net_sql,
46                            FS::cust_bill->owed_sql,
47                          )
48                    );
49     $count_addl = [ '$%.2f invoiced (gross)',
50                     '$%.2f invoiced (net)',
51                     '$%.2f outstanding balance',
52                   ];
53   }
54   $count_query .=  " FROM cust_bill $join_cust_main $extra_sql";
55
56   #$sql_query =
57   +{
58     'table'     => 'cust_bill',
59     'addl_from' => $join_cust_main,
60     'hashref'   => {},
61     'select'    => join(', ',
62                      'cust_bill.*',
63                      #( map "cust_main.$_", qw(custnum last first company) ),
64                      'cust_main.custnum as cust_main_custnum',
65                      FS::UI::Web::cust_sql_fields(),
66                      #$class->owed_sql. ' AS owed',
67                      #$class->net_sql.  ' AS net',
68                      FS::cust_bill->owed_sql. ' AS owed',
69                      FS::cust_bill->net_sql.  ' AS net',
70                    ),
71     'extra_sql' => $extra_sql,
72     'order_by'  => 'ORDER BY '. ( $params->{'order_by'} || 'cust_bill._date' ),
73
74     'count_query' => $count_query,
75     'count_addl'  => $count_addl,
76   };
77
78 }
79
80 =item search_sql_where HASHREF
81
82 Class method which returns an SQL WHERE fragment to search for parameters
83 specified in HASHREF.  Valid parameters are
84
85 =over 4
86
87 =item _date
88
89 List reference of start date, end date, as UNIX timestamps.
90
91 =item invnum_min
92
93 =item invnum_max
94
95 =item agentnum
96
97 =item cust_status
98
99 =item cust_classnum
100
101 List reference
102
103 =item charged
104
105 List reference of charged limits (exclusive).
106
107 =item owed
108
109 List reference of charged limits (exclusive).
110
111 =item open
112
113 flag, return open invoices only
114
115 =item net
116
117 flag, return net invoices only
118
119 =item days
120
121 =item newest_percust
122
123 =item custnum
124
125 Return only invoices belonging to that customer.
126
127 =item cust_classnum
128
129 Limit to that customer class (single value or arrayref).
130
131 =item payby
132
133 Limit to customers with that payment method (single value or arrayref).
134
135 =item refnum
136
137 Limit to customers with that advertising source.
138
139 =back
140
141 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
142
143 =cut
144
145 sub search_sql_where {
146   my($class, $param) = @_;
147   #if ( $cust_bill::DEBUG ) {
148   #  warn "$me search_sql_where called with params: \n".
149   #       join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
150   #}
151
152   #some false laziness w/cust_bill::re_X
153
154   my @search = ();
155
156   #agentnum
157   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
158     push @search, "cust_main.agentnum = $1";
159   }
160
161   #refnum
162   if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
163     push @search, "cust_main.refnum = $1";
164   }
165
166   #custnum
167   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
168     push @search, "cust_bill.custnum = $1";
169   }
170
171   #cust_status
172   if ( $param->{'cust_status'} =~ /^([a-z]+)$/ ) {
173     push @search, FS::cust_main->cust_status_sql . " = '$1' ";
174   }
175
176   #customer classnum (false laziness w/ cust_main/Search.pm)
177   if ( $param->{'cust_classnum'} ) {
178
179     my @classnum = ref( $param->{'cust_classnum'} )
180                      ? @{ $param->{'cust_classnum'} }
181                      :  ( $param->{'cust_classnum'} );
182
183     @classnum = grep /^(\d*)$/, @classnum;
184
185     if ( @classnum ) {
186       push @search, '( '. join(' OR ', map {
187                                              $_ ? "cust_main.classnum = $_"
188                                                 : "cust_main.classnum IS NULL"
189                                            }
190                                            @classnum
191                               ).
192                     ' )';
193     }
194
195   }
196
197   #payby
198   if ( $param->{payby} ) {
199     my $payby = $param->{payby};
200     $payby = [ $payby ] unless ref $payby;
201     my $payby_in = join(',', map {dbh->quote($_)} @$payby);
202     push @search, "cust_main.payby IN($payby_in)" if length($payby_in);
203   }
204
205   #_date
206   if ( $param->{_date} ) {
207     my($beginning, $ending) = @{$param->{_date}};
208
209     push @search, "cust_bill._date >= $beginning",
210                   "cust_bill._date <  $ending";
211   }
212
213   #invnum
214   if ( $param->{'invnum_min'} =~ /^\s*(\d+)\s*$/ ) {
215     push @search, "cust_bill.invnum >= $1";
216   }
217   if ( $param->{'invnum_max'} =~ /^\s*(\d+)\s*$/ ) {
218     push @search, "cust_bill.invnum <= $1";
219   }
220
221   #charged
222   if ( $param->{charged} ) {
223     my @charged = ref($param->{charged})
224                     ? @{ $param->{charged} }
225                     : ($param->{charged});
226
227     push @search, map { s/^charged/cust_bill.charged/; $_; }
228                       @charged;
229   }
230
231   my $owed_sql = FS::cust_bill->owed_sql;
232
233   #owed
234   if ( $param->{owed} ) {
235     my @owed = ref($param->{owed})
236                  ? @{ $param->{owed} }
237                  : ($param->{owed});
238     push @search, map { s/^owed/$owed_sql/; $_; }
239                       @owed;
240   }
241
242   #open/net flags
243   push @search, "0 != $owed_sql"
244     if $param->{'open'};
245   push @search, '0 != '. FS::cust_bill->net_sql
246     if $param->{'net'};
247
248   #days
249   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
250     if $param->{'days'};
251
252   #newest_percust
253   if ( $param->{'newest_percust'} ) {
254
255     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
256     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
257
258     my @newest_where = map { my $x = $_;
259                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
260                              $x;
261                            }
262                            grep ! /^cust_main./, @search;
263     my $newest_where = scalar(@newest_where)
264                          ? ' AND '. join(' AND ', @newest_where)
265                          : '';
266
267
268     push @search, "cust_bill._date = (
269       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
270         WHERE newest_cust_bill.custnum = cust_bill.custnum
271           $newest_where
272     )";
273
274   }
275
276   #promised_date - also has an option to accept nulls
277   if ( $param->{promised_date} ) {
278     my($beginning, $ending, $null) = @{$param->{promised_date}};
279
280     push @search, "(( cust_bill.promised_date >= $beginning AND ".
281                     "cust_bill.promised_date <  $ending )" .
282                     ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
283   }
284
285   #agent virtualization
286   my $curuser = $FS::CurrentUser::CurrentUser;
287   if ( $curuser->username eq 'fs_queue'
288        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
289     my $username = $1;
290     my $newuser = qsearchs('access_user', {
291       'username' => $username,
292       'disabled' => '',
293     } );
294     if ( $newuser ) {
295       $curuser = $newuser;
296     } else {
297       #warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
298       warn "[FS::cust_bill::Search] WARNING: (fs_queue) can't find CurrentUser $username\n";
299     }
300   }
301   push @search, $curuser->agentnums_sql;
302
303   join(' AND ', @search );
304
305 }
306
307 1;
308