Adding line 246 "edit global pockage definitions costs" back in
[freeside.git] / FS / FS / cust_pkg / Search.pm
1 package FS::cust_pkg::Search;
2
3 use strict;
4 use FS::CurrentUser;
5 use FS::UI::Web;
6 use FS::cust_main;
7 use FS::cust_pkg;
8
9 =item search HASHREF
10
11 (Class method)
12
13 Returns a qsearch hash expression to search for parameters specified in HASHREF.
14 Valid parameters are
15
16 =over 4
17
18 =item agentnum
19
20 =item magic
21
22 active, inactive, suspended, cancel (or cancelled)
23
24 =item status
25
26 active, inactive, suspended, one-time charge, inactive, cancel (or cancelled)
27
28 =item custom
29
30  boolean selects custom packages
31
32 =item classnum
33
34 =item pkgpart
35
36 pkgpart or arrayref or hashref of pkgparts
37
38 =item setup
39
40 arrayref of beginning and ending epoch date
41
42 =item last_bill
43
44 arrayref of beginning and ending epoch date
45
46 =item bill
47
48 arrayref of beginning and ending epoch date
49
50 =item adjourn
51
52 arrayref of beginning and ending epoch date
53
54 =item susp
55
56 arrayref of beginning and ending epoch date
57
58 =item expire
59
60 arrayref of beginning and ending epoch date
61
62 =item cancel
63
64 arrayref of beginning and ending epoch date
65
66 =item query
67
68 pkgnum or APKG_pkgnum
69
70 =item cust_fields
71
72 a value suited to passing to FS::UI::Web::cust_header
73
74 =item CurrentUser
75
76 specifies the user for agent virtualization
77
78 =item fcc_line
79
80 boolean; if true, returns only packages with more than 0 FCC phone lines.
81
82 =item state, country
83
84 Limit to packages with a service location in the specified state and country.
85 For FCC 477 reporting, mostly.
86
87 =item location_cust
88
89 Limit to packages whose service locations are the same as the customer's 
90 default service location.
91
92 =item location_nocust
93
94 Limit to packages whose service locations are not the customer's default 
95 service location.
96
97 =item location_census
98
99 Limit to packages whose service locations have census tracts.
100
101 =item location_nocensus
102
103 Limit to packages whose service locations do not have a census tract.
104
105 =item location_geocode
106
107 Limit to packages whose locations have geocodes.
108
109 =item location_geocode
110
111 Limit to packages whose locations do not have geocodes.
112
113 =back
114
115 =cut
116
117 sub search {
118   my ($class, $params) = @_;
119   my @where = ();
120
121   ##
122   # parse agent
123   ##
124
125   if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
126     push @where,
127       "cust_main.agentnum = $1";
128   }
129
130   ##
131   # parse cust_status
132   ##
133
134   if ( $params->{'cust_status'} =~ /^([a-z]+)$/ ) {
135     push @where, FS::cust_main->cust_status_sql . " = '$1' ";
136   }
137
138   ##
139   # parse customer sales person
140   ##
141
142   if ( $params->{'cust_main_salesnum'} =~ /^(\d+)$/ ) {
143     push @where, ($1 > 0) ? "cust_main.salesnum = $1"
144                           : 'cust_main.salesnum IS NULL';
145   }
146
147
148   ##
149   # parse sales person
150   ##
151
152   if ( $params->{'salesnum'} =~ /^(\d+)$/ ) {
153     push @where, ($1 > 0) ? "cust_pkg.salesnum = $1"
154                           : 'cust_pkg.salesnum IS NULL';
155   }
156
157   ##
158   # parse custnum
159   ##
160
161   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
162     push @where,
163       "cust_pkg.custnum = $1";
164   }
165
166   ##
167   # custbatch
168   ##
169
170   if ( $params->{'pkgbatch'} =~ /^([\w\/\-\:\.]+)$/ and $1 ) {
171     push @where,
172       "cust_pkg.pkgbatch = '$1'";
173   }
174
175   ##
176   # parse status
177   ##
178
179   if (    $params->{'magic'}  eq 'active'
180        || $params->{'status'} eq 'active' ) {
181
182     push @where, FS::cust_pkg->active_sql();
183
184   } elsif (    $params->{'magic'}  =~ /^not[ _]yet[ _]billed$/
185             || $params->{'status'} =~ /^not[ _]yet[ _]billed$/ ) {
186
187     push @where, FS::cust_pkg->not_yet_billed_sql();
188
189   } elsif (    $params->{'magic'}  =~ /^(one-time charge|inactive)/
190             || $params->{'status'} =~ /^(one-time charge|inactive)/ ) {
191
192     push @where, FS::cust_pkg->inactive_sql();
193
194   } elsif (    $params->{'magic'}  eq 'suspended'
195             || $params->{'status'} eq 'suspended'  ) {
196
197     push @where, FS::cust_pkg->suspended_sql();
198
199   } elsif (    $params->{'magic'}  =~ /^cancell?ed$/
200             || $params->{'status'} =~ /^cancell?ed$/ ) {
201
202     push @where, FS::cust_pkg->cancelled_sql();
203
204   }
205
206   ###
207   # parse package class
208   ###
209
210   if ( exists($params->{'classnum'}) ) {
211
212     my @classnum = ();
213     if ( ref($params->{'classnum'}) ) {
214
215       if ( ref($params->{'classnum'}) eq 'HASH' ) {
216         @classnum = grep $params->{'classnum'}{$_}, keys %{ $params->{'classnum'} };
217       } elsif ( ref($params->{'classnum'}) eq 'ARRAY' ) {
218         @classnum = @{ $params->{'classnum'} };
219       } else {
220         die 'unhandled classnum ref '. $params->{'classnum'};
221       }
222
223
224     } elsif ( $params->{'classnum'} =~ /^(\d*)$/ && $1 ne '0' ) {
225       @classnum = ( $1 );
226     }
227
228     if ( @classnum ) {
229
230       my @c_where = ();
231       my @nums = grep $_, @classnum;
232       push @c_where, 'part_pkg.classnum IN ('. join(',',@nums). ')' if @nums;
233       my $null = scalar( grep { $_ eq '' } @classnum );
234       push @c_where, 'part_pkg.classnum IS NULL' if $null;
235
236       if ( scalar(@c_where) == 1 ) {
237         push @where, @c_where;
238       } elsif ( @c_where ) {
239         push @where, ' ( '. join(' OR ', @c_where). ' ) ';
240       }
241
242     }
243     
244
245   }
246
247   ###
248   # parse package report options
249   ###
250
251   my @report_option = ();
252   if ( exists($params->{'report_option'}) ) {
253     if ( ref($params->{'report_option'}) eq 'ARRAY' ) {
254       @report_option = @{ $params->{'report_option'} };
255     } elsif ( $params->{'report_option'} =~ /^([,\d]*)$/ ) {
256       @report_option = split(',', $1);
257     }
258
259   }
260
261   if (@report_option) {
262     # this will result in the empty set for the dangling comma case as it should
263     push @where, 
264       map{ "0 < ( SELECT count(*) FROM part_pkg_option
265                     WHERE part_pkg_option.pkgpart = part_pkg.pkgpart
266                     AND optionname = 'report_option_$_'
267                     AND optionvalue = '1' )"
268          } @report_option;
269   }
270
271   foreach my $any ( grep /^report_option_any/, keys %$params ) {
272
273     my @report_option_any = ();
274     if ( ref($params->{$any}) eq 'ARRAY' ) {
275       @report_option_any = @{ $params->{$any} };
276     } elsif ( $params->{$any} =~ /^([,\d]*)$/ ) {
277       @report_option_any = split(',', $1);
278     }
279
280     if (@report_option_any) {
281       # this will result in the empty set for the dangling comma case as it should
282       push @where, ' ( '. join(' OR ',
283         map{ "0 < ( SELECT count(*) FROM part_pkg_option
284                       WHERE part_pkg_option.pkgpart = part_pkg.pkgpart
285                       AND optionname = 'report_option_$_'
286                       AND optionvalue = '1' )"
287            } @report_option_any
288       ). ' ) ';
289     }
290
291   }
292
293   ###
294   # parse custom
295   ###
296
297   push @where,  "part_pkg.custom = 'Y'" if $params->{custom};
298
299   ###
300   # parse fcc_line
301   ###
302
303   push @where,  "(part_pkg.fcc_ds0s > 0 OR pkg_class.fcc_ds0s > 0)" 
304                                                         if $params->{fcc_line};
305
306   ###
307   # parse censustract
308   ###
309
310   if ( exists($params->{'censustract'}) ) {
311     $params->{'censustract'} =~ /^([.\d]*)$/;
312     my $censustract = "cust_location.censustract = '$1'";
313     $censustract .= ' OR cust_location.censustract is NULL' unless $1;
314     push @where,  "( $censustract )";
315   }
316
317   ###
318   # parse censustract2
319   ###
320   if ( exists($params->{'censustract2'})
321        && $params->{'censustract2'} =~ /^(\d*)$/
322      )
323   {
324     if ($1) {
325       push @where, "cust_location.censustract LIKE '$1%'";
326     } else {
327       push @where,
328         "( cust_location.censustract = '' OR cust_location.censustract IS NULL )";
329     }
330   }
331
332   ###
333   # parse country/state
334   ###
335   for (qw(state country)) { # parsing rules are the same for these
336   if ( exists($params->{$_}) 
337     && uc($params->{$_}) =~ /^([A-Z]{2})$/ )
338     {
339       # XXX post-2.3 only--before that, state/country may be in cust_main
340       push @where, "cust_location.$_ = '$1'";
341     }
342   }
343
344   ###
345   # location_* flags
346   ###
347   if ( $params->{location_cust} xor $params->{location_nocust} ) {
348     my $op = $params->{location_cust} ? '=' : '!=';
349     push @where, "cust_location.locationnum $op cust_main.ship_locationnum";
350   }
351   if ( $params->{location_census} xor $params->{location_nocensus} ) {
352     my $op = $params->{location_census} ? "IS NOT NULL" : "IS NULL";
353     push @where, "cust_location.censustract $op";
354   }
355   if ( $params->{location_geocode} xor $params->{location_nogeocode} ) {
356     my $op = $params->{location_geocode} ? "IS NOT NULL" : "IS NULL";
357     push @where, "cust_location.geocode $op";
358   }
359
360   ###
361   # parse part_pkg
362   ###
363
364   if ( ref($params->{'pkgpart'}) ) {
365
366     my @pkgpart = ();
367     if ( ref($params->{'pkgpart'}) eq 'HASH' ) {
368       @pkgpart = grep $params->{'pkgpart'}{$_}, keys %{ $params->{'pkgpart'} };
369     } elsif ( ref($params->{'pkgpart'}) eq 'ARRAY' ) {
370       @pkgpart = @{ $params->{'pkgpart'} };
371     } else {
372       die 'unhandled pkgpart ref '. $params->{'pkgpart'};
373     }
374
375     @pkgpart = grep /^(\d+)$/, @pkgpart;
376
377     push @where, 'pkgpart IN ('. join(',', @pkgpart). ')' if scalar(@pkgpart);
378
379   } elsif ( $params->{'pkgpart'} =~ /^(\d+)$/ ) {
380     push @where, "pkgpart = $1";
381   } 
382
383   ###
384   # parse dates
385   ###
386
387   my $orderby = '';
388
389   #false laziness w/report_cust_pkg.html
390   my %disable = (
391     'all'             => {},
392     'one-time charge' => { 'last_bill'=>1, 'bill'=>1, 'adjourn'=>1, 'susp'=>1, 'expire'=>1, 'cancel'=>1, },
393     'active'          => { 'susp'=>1, 'cancel'=>1 },
394     'suspended'       => { 'cancel' => 1 },
395     'cancelled'       => {},
396     ''                => {},
397   );
398
399   if( exists($params->{'active'} ) ) {
400     # This overrides all the other date-related fields
401     my($beginning, $ending) = @{$params->{'active'}};
402     push @where,
403       "cust_pkg.setup IS NOT NULL",
404       "cust_pkg.setup <= $ending",
405       "(cust_pkg.cancel IS NULL OR cust_pkg.cancel >= $beginning )",
406       "NOT (".FS::cust_pkg->onetime_sql . ")";
407   }
408   else {
409     foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel )) {
410
411       next unless exists($params->{$field});
412
413       my($beginning, $ending) = @{$params->{$field}};
414
415       next if $beginning == 0 && $ending == 4294967295;
416
417       push @where,
418         "cust_pkg.$field IS NOT NULL",
419         "cust_pkg.$field >= $beginning",
420         "cust_pkg.$field <= $ending";
421
422       $orderby ||= "ORDER BY cust_pkg.$field";
423
424     }
425   }
426
427   $orderby ||= 'ORDER BY bill';
428
429   ###
430   # parse magic, legacy, etc.
431   ###
432
433   if ( $params->{'magic'} &&
434        $params->{'magic'} =~ /^(active|inactive|suspended|cancell?ed)$/
435   ) {
436
437     $orderby = 'ORDER BY pkgnum';
438
439     if ( $params->{'pkgpart'} =~ /^(\d+)$/ ) {
440       push @where, "pkgpart = $1";
441     }
442
443   } elsif ( $params->{'query'} eq 'pkgnum' ) {
444
445     $orderby = 'ORDER BY pkgnum';
446
447   } elsif ( $params->{'query'} eq 'APKG_pkgnum' ) {
448
449     $orderby = 'ORDER BY pkgnum';
450
451     push @where, '0 < (
452       SELECT count(*) FROM pkg_svc
453        WHERE pkg_svc.pkgpart =  cust_pkg.pkgpart
454          AND pkg_svc.quantity > ( SELECT count(*) FROM cust_svc
455                                    WHERE cust_svc.pkgnum  = cust_pkg.pkgnum
456                                      AND cust_svc.svcpart = pkg_svc.svcpart
457                                 )
458     )';
459   
460   }
461
462   ##
463   # setup queries, links, subs, etc. for the search
464   ##
465
466   # here is the agent virtualization
467   if ($params->{CurrentUser}) {
468     my $access_user =
469       qsearchs('access_user', { username => $params->{CurrentUser} });
470
471     if ($access_user) {
472       push @where, $access_user->agentnums_sql('table'=>'cust_main');
473     } else {
474       push @where, "1=0";
475     }
476   } else {
477     push @where, $FS::CurrentUser::CurrentUser->agentnums_sql('table'=>'cust_main');
478   }
479
480   my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
481
482   my $addl_from = 'LEFT JOIN part_pkg  USING ( pkgpart  ) '.
483                   'LEFT JOIN pkg_class ON ( part_pkg.classnum = pkg_class.classnum ) '.
484                   'LEFT JOIN cust_location USING ( locationnum ) '.
485                   FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
486
487   my $select;
488   my $count_query;
489   if ( $params->{'select_zip5'} ) {
490     my $zip = 'cust_location.zip';
491
492     $select = "DISTINCT substr($zip,1,5) as zip";
493     $orderby = "ORDER BY substr($zip,1,5)";
494     $count_query = "SELECT COUNT( DISTINCT substr($zip,1,5) )";
495   } else {
496     $select = join(', ',
497                          'cust_pkg.*',
498                          ( map "part_pkg.$_", qw( pkg freq ) ),
499                          'pkg_class.classname',
500                          'cust_main.custnum AS cust_main_custnum',
501                          FS::UI::Web::cust_sql_fields(
502                            $params->{'cust_fields'}
503                          ),
504                   );
505     $count_query = 'SELECT COUNT(*)';
506   }
507
508   $count_query .= " FROM cust_pkg $addl_from $extra_sql";
509
510   my $sql_query = {
511     'table'       => 'cust_pkg',
512     'hashref'     => {},
513     'select'      => $select,
514     'extra_sql'   => $extra_sql,
515     'order_by'    => $orderby,
516     'addl_from'   => $addl_from,
517     'count_query' => $count_query,
518   };
519
520 }
521
522 1;
523