]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/actor.pm
Address long-standing typo: clense_ISO8601, matching change to OpenSRF-trunk.
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Storage / Publisher / actor.pm
1 package OpenILS::Application::Storage::Publisher::actor;
2 use base qw/OpenILS::Application::Storage/;
3 use OpenILS::Application::Storage::CDBI::actor;
4 use OpenSRF::Utils::Logger qw/:level/;
5 use OpenSRF::Utils qw/:datetime/;
6 use OpenILS::Utils::Fieldmapper;
7 use OpenSRF::Utils::SettingsClient;
8
9 use DateTime;           
10 use DateTime::Format::ISO8601;  
11 use DateTime::Set;
12 use DateTime::SpanSet;
13                                                 
14                                                                                                 
15 my $_dt_parser = DateTime::Format::ISO8601->new;    
16
17 my $log = 'OpenSRF::Utils::Logger';
18
19 sub new_usergroup_id {
20         return actor::user->db_Main->selectrow_array("select nextval('actor.usr_usrgroup_seq'::regclass)");
21 }
22 __PACKAGE__->register_method(
23         api_name        => 'open-ils.storage.actor.user.group_id.new',
24         api_level       => 1,
25         method          => 'new_usergroup_id',
26 );
27
28 sub juv_to_adult {
29         my $self = shift;
30         my $client = shift;
31         my $adult_age = shift;
32
33         my $sql = <<"   SQL";
34             UPDATE  actor.usr
35               SET   juvenile = FALSE
36               WHERE AGE(dob) > ?::INTERVAL;
37         SQL
38
39     my $sth = actor::user->db_Main->prepare_cached($sql);
40     $sth->execute($adult_age);
41
42     return $sth->rows;
43 }
44 __PACKAGE__->register_method(
45         api_name        => 'open-ils.storage.actor.user.juvenile_to_adult',
46         api_level       => 1,
47         method          => 'juv_to_adult',
48 );
49
50 sub usr_total_owed {
51         my $self = shift;
52         my $client = shift;
53         my $usr = shift;
54
55         my $sql = <<"   SQL";
56                         SELECT  x.usr,
57                                         SUM(COALESCE((SELECT SUM(b.amount) FROM money.billing b WHERE b.voided IS FALSE AND b.xact = x.id),0.0)) -
58                                                 SUM(COALESCE((SELECT SUM(p.amount) FROM money.payment p WHERE p.voided IS FALSE AND p.xact = x.id),0.0))
59                           FROM  money.billable_xact x
60                           WHERE x.usr = ? AND x.xact_finish IS NULL
61                           GROUP BY 1
62         SQL
63
64         my (undef,$val) = actor::user->db_Main->selectrow_array($sql, {}, $usr);
65
66         return $val;
67 }
68 __PACKAGE__->register_method(
69         api_name        => 'open-ils.storage.actor.user.total_owed',
70         api_level       => 1,
71         method          => 'usr_total_owed',
72 );
73
74 sub usr_breakdown_out {
75         my $self = shift;
76         my $client = shift;
77         my $usr = shift;
78
79         my $out_sql = <<"       SQL";
80                         SELECT  id
81                           FROM  action.circulation
82                           WHERE usr = ?
83                     AND checkin_time IS NULL
84                     AND (  (fine_interval >= '1 day' AND due_date >= 'today')
85                         OR (fine_interval < '1 day'  AND due_date > 'now'   ))
86                     AND (stop_fines IS NULL
87                         OR stop_fines NOT IN ('LOST','CLAIMSRETURNED','LONGOVERDUE'))
88         SQL
89
90         my $out = actor::user->db_Main->selectcol_arrayref($out_sql, {}, $usr);
91
92         my $od_sql = <<"        SQL";
93                         SELECT  id
94                           FROM  action.circulation
95                           WHERE usr = ?
96                     AND checkin_time IS NULL
97                     AND (  (fine_interval >= '1 day' AND due_date < 'today')
98                         OR (fine_interval < '1 day'  AND due_date < 'now'  ))
99                     AND (stop_fines IS NULL
100                         OR stop_fines NOT IN ('LOST','CLAIMSRETURNED','LONGOVERDUE'))
101         SQL
102
103         my $od = actor::user->db_Main->selectcol_arrayref($od_sql, {}, $usr);
104
105         my $lost_sql = <<"      SQL";
106                         SELECT  id
107                           FROM  action.circulation
108                           WHERE usr = ? AND checkin_time IS NULL AND xact_finish IS NULL AND stop_fines = 'LOST'
109         SQL
110
111         my $lost = actor::user->db_Main->selectcol_arrayref($lost_sql, {}, $usr);
112
113         my $cl_sql = <<"        SQL";
114                         SELECT  id
115                           FROM  action.circulation
116                           WHERE usr = ? AND checkin_time IS NULL AND stop_fines = 'CLAIMSRETURNED'
117         SQL
118
119         my $cl = actor::user->db_Main->selectcol_arrayref($cl_sql, {}, $usr);
120
121         my $lo_sql = <<"        SQL";
122                         SELECT  id
123                           FROM  action.circulation
124                           WHERE usr = ? AND checkin_time IS NULL AND stop_fines = 'LONGOVERDUE'
125         SQL
126
127         my $lo = actor::user->db_Main->selectcol_arrayref($lo_sql, {}, $usr);
128
129         if ($self->api_name =~/count$/o) {
130                 return {        total   => scalar(@$out) + scalar(@$od) + scalar(@$lost) + scalar(@$cl) + scalar(@$lo),
131                                         out             => scalar(@$out),
132                                         overdue => scalar(@$od),
133                                         lost    => scalar(@$lost),
134                                         claims_returned => scalar(@$cl),
135                                         long_overdue            => scalar(@$lo),
136                 };
137         }
138
139         return {        out             => $out,
140                                 overdue => $od,
141                                 lost    => $lost,
142                                 claims_returned => $cl,
143                                 long_overdue            => $lo,
144         };
145 }
146 __PACKAGE__->register_method(
147         api_name        => 'open-ils.storage.actor.user.checked_out',
148         api_level       => 1,
149         method          => 'usr_breakdown_out',
150 );
151 __PACKAGE__->register_method(
152         api_name        => 'open-ils.storage.actor.user.checked_out.count',
153         api_level       => 1,
154         method          => 'usr_breakdown_out',
155 );
156
157 sub usr_total_out {
158         my $self = shift;
159         my $client = shift;
160         my $usr = shift;
161
162         my $sql = <<"   SQL";
163                         SELECT  count(*)
164                           FROM  action.circulation
165                           WHERE usr = ? AND checkin_time IS NULL
166         SQL
167
168         my ($val) = actor::user->db_Main->selectrow_array($sql, {}, $usr);
169
170         return $val;
171 }
172 __PACKAGE__->register_method(
173         api_name        => 'open-ils.storage.actor.user.total_out',
174         api_level       => 1,
175         method          => 'usr_total_out',
176 );
177
178 sub calc_proximity {
179         my $self = shift;
180         my $client = shift;
181
182         local $OpenILS::Application::Storage::WRITE = 1;
183
184         my $delete_sql = <<"    SQL";
185                 DELETE FROM actor.org_unit_proximity;
186         SQL
187
188         my $insert_sql = <<"    SQL";
189                 INSERT INTO actor.org_unit_proximity (from_org, to_org, prox)
190                         SELECT  l.id,
191                                 r.id,
192                                 actor.org_unit_proximity(l.id,r.id)
193                           FROM  actor.org_unit l,
194                                 actor.org_unit r;
195         SQL
196
197         actor::org_unit_proximity->db_Main->do($delete_sql);
198         actor::org_unit_proximity->db_Main->do($insert_sql);
199
200         return 1;
201 }
202 __PACKAGE__->register_method(
203         api_name        => 'open-ils.storage.actor.org_unit.refresh_proximity',
204         api_level       => 1,
205         method          => 'calc_proximity',
206 );
207
208 sub make_hoo_spanset {
209     my $hoo = shift;
210     return undef unless $hoo;
211
212     my $today = shift || DateTime->now;
213
214     my $tz = OpenSRF::AppSession->create('open-ils.actor')->request(
215         'open-ils.actor.ou_setting.ancestor_default' => $hoo->id.'' => 'org_unit.timezone'
216     )->gather(1) || DateTime::TimeZone->new( name => 'local' )->name;
217
218     my $current_dow = $today->day_of_week_0;
219
220     my $spanset = DateTime::SpanSet->empty_set;
221     for my $d ( 0 .. 6 ) {
222
223         my $omethod = 'dow_'.$d.'_open';
224         my $cmethod = 'dow_'.$d.'_close';
225
226         my $open = interval_to_seconds($hoo->$omethod());
227         my $close = interval_to_seconds($hoo->$cmethod());
228
229         next if ($open == $close && $open == 0);
230
231         my $dow_offset = ($d - $current_dow) * $one_day;
232         $close += $one_day if ($close <= $open);
233
234         $spanset = $spanset->union(
235             DateTime::Span->new(
236                 start => $today->clone->add( seconds => $dow_offset + $open  ),
237                 end   => $today->clone->add( seconds => $dow_offset + $close )
238             )
239         );
240     }
241
242     return $spanset->complement;
243 }
244
245 sub make_closure_spanset {
246     my $closures = shift;
247     return undef unless $closures;
248
249     my $spanset = DateTime::SpanSet->empty_set;
250     for my $k ( keys %$closures ) {
251         my $c = $$closures{$k};
252
253         $spanset = $spanset->union(
254             DateTime::Span->new(
255                 start => $_dt_parser->parse_datetime(cleanse_ISO8601($c->{close_start})),
256                 end   => $_dt_parser->parse_datetime(cleanse_ISO8601($c->{close_end}))
257             )
258         );
259     }
260
261     return $spanset;
262 }
263
264 sub new_org_closed_overlap {
265         my $self = shift;
266         my $client = shift;
267         my $ou = shift;
268         my $date = shift;
269         my $direction = shift || 0;
270         my $no_hoo = shift || 0;
271
272         return undef unless ($date && $ou);
273
274     # we're given a date and a direction, find any closures that contain the date
275         my $t = actor::org_unit::closed_date->table;
276         my $sql = <<"   SQL";
277                 SELECT  *
278                   FROM  $t
279                   WHERE close_end > ?
280                         AND org_unit = ?
281                   ORDER BY close_start ASC, close_end DESC
282                   LIMIT 1
283         SQL
284
285         $date = cleanse_ISO8601($date);
286
287     my $target_date = $_dt_parser->parse_datetime( $date );
288         my ($begin, $end) = ($target_date, $target_date);
289
290     # create a spanset from the closures that contain the $date
291         my $closure_spanset = make_closure_spanset(
292         actor::org_unit::closed_date->db_Main->selectall_hashref( $sql, 'id', {}, $date, $ou )
293     );
294
295     if ($closure_spanset && $closure_spanset->intersects( $target_date )) {
296         my $closure_intersection = $closure_spanset->intersection( $target_date );
297         $begin = $closure_intersection->min;
298         $end = $closure_intersection->max;
299
300                 if ( $direction <= 0 ) {
301                         $begin->subtract( minutes => 1 );
302
303                         while ( my $_b = new_org_closed_overlap($self, $client, $ou, $begin->strftime('%FT%T%z'), -1, 1 ) ) {
304                                 $begin = $_dt_parser->parse_datetime( cleanse_ISO8601($_b->{start}) );
305                         }
306                 }
307
308                 if ( $direction >= 0 ) {
309                         $end->add( minutes => 1 );
310
311                         while ( my $_a = new_org_closed_overlap($self, $client, $ou, $end->strftime('%FT%T%z'), 1, 1 ) ) {
312                                 $end = $_dt_parser->parse_datetime( cleanse_ISO8601($_a->{end}) );
313                         }
314                 }
315     }
316
317         if ( !$no_hoo ) {
318
319             my $begin_hoo = make_hoo_spanset(actor::org_unit::hours_of_operation->retrieve($ou), $begin);
320             my $end_hoo   = make_hoo_spanset(actor::org_unit::hours_of_operation->retrieve($ou), $end  );
321
322
323         if ( $begin_hoo && $direction <= 0 && $begin_hoo->intersects($begin) ) {
324             my $hoo_intersection = $begin_hoo->intersection( $begin );
325             $begin = $hoo_intersection->min;
326             $begin->subtract( minutes => 1 );
327
328             while ( my $_b = new_org_closed_overlap($self, $client, $ou, $begin->strftime('%FT%T%z'), -1 ) ) {
329                 $begin = $_dt_parser->parse_datetime( cleanse_ISO8601($_b->{start}) );
330             }
331         }
332         
333         if ( $end_hoo && $direction >= 0 && $end_hoo->intersects($end) ) {
334             my $hoo_intersection = $end_hoo->intersection( $end );
335             $end = $hoo_intersection->max;
336                         $end->add( minutes => 1 );
337
338
339             while ( my $_b = new_org_closed_overlap($self, $client, $ou, $end->strftime('%FT%T%z'), -1 ) ) {
340                 $end = $_dt_parser->parse_datetime( cleanse_ISO8601($_b->{end}) );
341             }
342         }
343     }
344
345     my $start = $begin->strftime('%FT%T%z');
346     my $stop = $end->strftime('%FT%T%z');
347
348     return undef if ($start eq $stop);
349     return { start => $start, end => $stop };
350 }
351 __PACKAGE__->register_method(
352         api_name        => 'open-ils.storage.actor.org_unit.closed_date.overlap',
353         api_level       => 0,
354         method          => 'new_org_closed_overlap',
355 );
356
357 sub org_closed_overlap {
358     my $self = shift;
359     my $client = shift;
360     my $ou = shift;
361     my $date = shift;
362     my $direction = shift || 0;
363     my $no_hoo = shift || 0;
364
365     return undef unless ($date && $ou);
366
367     my $t = actor::org_unit::closed_date->table;
368     my $sql = <<"    SQL";
369         SELECT  *
370           FROM  $t
371           WHERE ? between close_start and close_end
372             AND org_unit = ?
373           ORDER BY close_start ASC, close_end DESC
374           LIMIT 1
375     SQL
376
377     $date = cleanse_ISO8601($date);
378     my ($begin, $end) = ($date,$date);
379
380     my $hoo = actor::org_unit::hours_of_operation->retrieve($ou);
381
382     if (my $closure = actor::org_unit::closed_date->db_Main->selectrow_hashref( $sql, {}, $date, $ou )) {
383         $begin = cleanse_ISO8601($closure->{close_start});
384         $end = cleanse_ISO8601($closure->{close_end});
385
386         if ( $direction <= 0 ) {
387             $before = $_dt_parser->parse_datetime( $begin );
388             $before->subtract( minutes => 1 );
389
390             while ( my $_b = org_closed_overlap($self, $client, $ou, $before->strftime('%FT%T%z'), -1, 1 ) ) {
391                 $before = $_dt_parser->parse_datetime( cleanse_ISO8601($_b->{start}) );
392             }
393             $begin = cleanse_ISO8601($before->strftime('%FT%T%z'));
394         }
395
396         if ( $direction >= 0 ) {
397             $after = $_dt_parser->parse_datetime( $end );
398             $after->add( minutes => 1 );
399
400             while ( my $_a = org_closed_overlap($self, $client, $ou, $after->strftime('%FT%T%z'), 1, 1 ) ) {
401                 $after = $_dt_parser->parse_datetime( cleanse_ISO8601($_a->{end}) );
402             }
403             $end = cleanse_ISO8601($after->strftime('%FT%T%z'));
404         }
405     }
406
407     if ( !$no_hoo ) {
408         if ( $hoo ) {
409
410             if ( $direction <= 0 ) {
411                 my $begin_dow = $_dt_parser->parse_datetime( $begin )->day_of_week_0;
412                 my $begin_open_meth = "dow_".$begin_dow."_open";
413                 my $begin_close_meth = "dow_".$begin_dow."_close";
414
415                 my $count = 1;
416                 while ($hoo->$begin_open_meth eq '00:00:00' and $hoo->$begin_close_meth eq '00:00:00') {
417                     $begin = cleanse_ISO8601($_dt_parser->parse_datetime( $begin )->subtract( days => 1)->strftime('%FT%T%z'));
418                     $begin_dow++;
419                     $begin_dow %= 7;
420                     $count++;
421                     last if ($count > 6);
422                     $begin_open_meth = "dow_".$begin_dow."_open";
423                     $begin_close_meth = "dow_".$begin_dow."_close";
424                 }
425
426                 if (my $closure = actor::org_unit::closed_date->db_Main->selectrow_hashref( $sql, {}, $begin, $ou )) {
427                     $before = $_dt_parser->parse_datetime( $begin );
428                     $before->subtract( minutes => 1 );
429                     while ( my $_b = org_closed_overlap($self, $client, $ou, $before->strftime('%FT%T%z'), -1 ) ) {
430                         $before = $_dt_parser->parse_datetime( cleanse_ISO8601($_b->{start}) );
431                     }
432                 }
433             }
434     
435             if ( $direction >= 0 ) {
436                 my $end_dow = $_dt_parser->parse_datetime( $end )->day_of_week_0;
437                 my $end_open_meth = "dow_".$end_dow."_open";
438                 my $end_close_meth = "dow_".$end_dow."_close";
439     
440                 $count = 1;
441                 while ($hoo->$end_open_meth eq '00:00:00' and $hoo->$end_close_meth eq '00:00:00') {
442                     $end = cleanse_ISO8601($_dt_parser->parse_datetime( $end )->add( days => 1)->strftime('%FT%T%z'));
443                     $end_dow++;
444                     $end_dow %= 7;
445                     $count++;
446                     last if ($count > 6);
447                     $end_open_meth = "dow_".$end_dow."_open";
448                     $end_close_meth = "dow_".$end_dow."_close";
449                 }
450
451                 if (my $closure = actor::org_unit::closed_date->db_Main->selectrow_hashref( $sql, {}, $end, $ou )) {
452                     $after = $_dt_parser->parse_datetime( $end );
453                     $after->add( minutes => 1 );
454
455                     while ( my $_a = org_closed_overlap($self, $client, $ou, $after->strftime('%FT%T%z'), 1 ) ) {
456                         $after = $_dt_parser->parse_datetime( cleanse_ISO8601($_a->{end}) );
457                     }
458                     $end = cleanse_ISO8601($after->strftime('%FT%T%z'));
459                 }
460             }
461
462         }
463     }
464
465     if ($begin eq $date && $end eq $date) {
466         return undef;
467     }
468
469     return { start => $begin, end => $end };
470 }
471 __PACKAGE__->register_method(
472         api_name        => 'open-ils.storage.actor.org_unit.closed_date.overlap',
473         api_level       => 1,
474         method          => 'org_closed_overlap',
475 );
476
477 sub user_by_barcode {
478         my $self = shift;
479         my $client = shift;
480         my @barcodes = shift;
481
482         return undef unless @barcodes;
483
484         for my $card ( actor::card->search( { barcode => @barcodes } ) ) {
485                 next unless $card;
486                 if (@barcodes == 1) {
487                         return $card->usr->to_fieldmapper;
488                 }
489                 $client->respond( $card->usr->to_fieldmapper);
490         }
491         return undef;
492 }
493 __PACKAGE__->register_method(
494         api_name        => 'open-ils.storage.direct.actor.user.search.barcode',
495         api_level       => 1,
496         method          => 'user_by_barcode',
497         stream          => 1,
498         cachable        => 1,
499 );
500
501 sub lost_barcodes {
502         my $self = shift;
503         my $client = shift;
504
505         my $c = actor::card->table;
506         my $p = actor::user->table;
507
508         my $sql = "SELECT c.barcode FROM $c c JOIN $p p ON (c.usr = p.id) WHERE p.card <> c.id";
509
510         my $list = actor::user->db_Main->selectcol_arrayref($sql);
511         for my $bc ( @$list ) {
512                 $client->respond($bc);
513         }
514         return undef;
515 }
516 __PACKAGE__->register_method(
517         api_name        => 'open-ils.storage.actor.user.lost_barcodes',
518         api_level       => 1,
519         stream          => 1,
520         method          => 'lost_barcodes',
521         signature       => <<'  NOTE',
522                 Returns an array of barcodes that belong to lost cards.
523                 @return array of barcodes
524         NOTE
525 );
526
527 sub expired_barcodes {
528         my $self = shift;
529         my $client = shift;
530
531         my $c = actor::card->table;
532         my $p = actor::user->table;
533
534         my $sql = "SELECT c.barcode FROM $c c JOIN $p p ON (c.usr = p.id) WHERE p.expire_date < CURRENT_DATE";
535
536         my $list = actor::user->db_Main->selectcol_arrayref($sql);
537         for my $bc ( @$list ) {
538                 $client->respond($bc);
539         }
540         return undef;
541 }
542 __PACKAGE__->register_method(
543         api_name        => 'open-ils.storage.actor.user.expired_barcodes',
544         api_level       => 1,
545         stream          => 1,
546         method          => 'expired_barcodes',
547         signature       => <<'  NOTE',
548                 Returns an array of barcodes that are currently expired.
549                 @return array of barcodes
550         NOTE
551 );
552
553 sub barred_barcodes {
554         my $self = shift;
555         my $client = shift;
556
557         my $c = actor::card->table;
558         my $p = actor::user->table;
559
560         my $sql = "SELECT c.barcode FROM $c c JOIN $p p ON (c.usr = p.id) WHERE p.barred IS TRUE";
561
562         my $list = actor::user->db_Main->selectcol_arrayref($sql);
563         for my $bc ( @$list ) {
564                 $client->respond($bc);
565         }
566         return undef;
567 }
568 __PACKAGE__->register_method(
569         api_name        => 'open-ils.storage.actor.user.barred_barcodes',
570         api_level       => 1,
571         stream          => 1,
572         method          => 'barred_barcodes',
573         signature       => <<'  NOTE',
574                 Returns an array of barcodes that are currently barred.
575                 @return array of barcodes
576         NOTE
577 );
578
579 sub penalized_barcodes {
580         my $self = shift;
581         my $client = shift;
582         my @ignore = @_;
583
584         my $c = actor::card->table;
585         my $p = actor::user_standing_penalty->table;
586
587         my $sql = "SELECT c.barcode FROM $c c JOIN $p p USING (usr)";
588
589         if (@ignore) {
590                 $sql .= ' WHERE penalty_type NOT IN ('. join(',', map { '?' } @ignore) . ')';
591         }
592
593         $sql .= ' GROUP BY c.barcode;';
594
595         my $list = actor::user->db_Main->selectcol_arrayref($sql, {}, @ignore);
596         for my $bc ( @$list ) {
597                 $client->respond($bc);
598         }
599         return undef;
600 }
601 __PACKAGE__->register_method(
602         api_name        => 'open-ils.storage.actor.user.penalized_barcodes',
603         api_level       => 1,
604         stream          => 1,
605         method          => 'penalized_barcodes',
606         signature       => <<'  NOTE',
607                 Returns an array of barcodes that have penalties not listed
608                 as a parameter.  Supply a list of any penalty types that should
609                 not stop a patron from checking out materials.
610
611                 @param ignore_list Penalty type to ignore
612                 @return array of barcodes
613         NOTE
614 );
615
616
617 sub patron_search {
618         my $self = shift;
619         my $client = shift;
620         my $search = shift;
621         my $limit = shift || 1000;
622         my $sort = shift;
623         my $inactive = shift;
624         my $ws_ou = shift;
625         my $ws_ou_depth = shift || 0;
626
627     my $penalty_sort = 0;
628
629         my $strict_opt_in = OpenSRF::Utils::SettingsClient->new->config_value( share => user => 'opt_in' );
630
631         $sort = ['family_name','first_given_name'] unless ($$sort[0]);
632         push @$sort,'id';
633
634     if ($$sort[0] eq 'penalties') {
635         shift @$sort;
636         $penalty_sort = 1;
637     }
638
639         # group 0 = user
640         # group 1 = address
641         # group 2 = phone, ident
642         # group 3 = barcode
643
644         my $usr = join ' AND ', map { "LOWER(CAST($_ AS text)) ~ ?" } grep { ''.$$search{$_}{group} eq '0' } keys %$search;
645         my @usrv = map { "^$$search{$_}{value}" } grep { ''.$$search{$_}{group} eq '0' } keys %$search;
646
647         my $addr = join ' AND ', map { "LOWER(CAST($_ AS text)) ~ ?" } grep { ''.$$search{$_}{group} eq '1' } keys %$search;
648         my @addrv = map { "^$$search{$_}{value}" } grep { ''.$$search{$_}{group} eq '1' } keys %$search;
649
650         my $pv = $$search{phone}{value};
651         my $iv = $$search{ident}{value};
652         my $nv = $$search{name}{value};
653         my $cv = $$search{card}{value};
654
655         my $card = '';
656         if ($cv) {
657             $card = 'JOIN (SELECT DISTINCT usr FROM actor.card WHERE barcode LIKE ?||\'%\') AS card ON (card.usr = users.id)';
658             unshift(@usrv, $cv);
659         }
660
661         my $phone = '';
662         my @ps;
663         my @phonev;
664         if ($pv) {
665                 for my $p ( qw/day_phone evening_phone other_phone/ ) {
666                         push @ps, "LOWER($p) ~ ?";
667                         push @phonev, "^$pv";
668                 }
669                 $phone = '(' . join(' OR ', @ps) . ')';
670         }
671
672         my $ident = '';
673         my @is;
674         my @identv;
675         if ($iv) {
676                 for my $i ( qw/ident_value ident_value2/ ) {
677                         push @is, "LOWER($i) ~ ?";
678                         push @identv, "^$iv";
679                 }
680                 $ident = '(' . join(' OR ', @is) . ')';
681         }
682
683         my $name = '';
684         my @ns;
685         my @namev;
686         if (0 && $nv) {
687                 for my $n ( qw/first_given_name second_given_name family_name/ ) {
688                         push @ns, "LOWER($n) ~ ?";
689                         push @namev, "^$nv";
690                 }
691                 $name = '(' . join(' OR ', @ns) . ')';
692         }
693
694         my $usr_where = join ' AND ', grep { $_ } ($usr,$phone,$ident,$name);
695         my $addr_where = $addr;
696
697
698         my $u_table = actor::user->table;
699         my $a_table = actor::user_address->table;
700         my $opt_in_table = actor::usr_org_unit_opt_in->table;
701         my $ou_table = actor::org_unit->table;
702
703         my $u_select = "SELECT id as id FROM $u_table u WHERE $usr_where";
704         my $a_select = "SELECT u.id as id FROM $a_table a JOIN $u_table u ON (u.mailing_address = a.id OR u.billing_address = a.id) WHERE $addr_where";
705
706         my $clone_select = '';
707
708         #$clone_select = "JOIN (SELECT cu.id as id FROM $a_table ca ".
709         #                  "JOIN $u_table cu ON (cu.mailing_address = ca.id OR cu.billing_address = ca.id) ".
710         #                  "WHERE $addr_where) AS clone ON (clone.id = users.id)" if ($addr_where);
711
712         my $select = '';
713         if ($usr_where) {
714                 if ($addr_where) {
715                         $select = "$u_select INTERSECT $a_select";
716                 } else {
717                         $select = $u_select;
718                 }
719         } elsif ($addr_where) {
720                 $select = "$a_select";
721         }
722
723         return undef if (!$select && !$card);
724
725         my $order_by = join ', ', map { 'LOWER(CAST(users.'. (split / /,$_)[0] . ' AS text)) ' . (split / /,$_)[1] } @$sort;
726         my $distinct_list = join ', ', map { 'LOWER(CAST(users.'. (split / /,$_)[0] . ' AS text))' } @$sort;
727     my $group_list = $distinct_list;
728
729         if ($inactive) {
730                 $inactive = '';
731         } else {
732                 $inactive = 'AND users.active = TRUE';
733         }
734
735         if (!$ws_ou) {  # XXX This should be required!!
736                 $ws_ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
737         }
738
739         my $opt_in_join = '';
740         my $opt_in_where = '';
741         if (lc($strict_opt_in) eq 'true') {
742                 $opt_in_join = "LEFT JOIN $opt_in_table oi ON (oi.org_unit = $ws_ou AND users.id = oi.usr)";
743                 $opt_in_where = "AND (oi.id IS NOT NULL OR users.home_ou = $ws_ou)";
744         }
745
746         my $penalty_join = '';
747     if ($penalty_sort) {
748         $distinct_list = 'COUNT(penalties.id), ' . $distinct_list;
749         $order_by = 'COUNT(penalties.id) DESC, ' . $order_by;
750         unshift @$sort, 'COUNT(penalties.id)';
751             $penalty_join = <<"        SQL";
752             LEFT JOIN actor.usr_standing_penalty penalties
753                 ON (users.id = penalties.usr AND (penalties.stop_date IS NULL OR penalties.stop_date > NOW()))
754         SQL
755     }
756
757         my $descendants = "actor.org_unit_descendants($ws_ou, $ws_ou_depth)";
758
759         $select = "JOIN ($select) AS search ON (search.id = users.id)" if ($select);
760         $select = <<"   SQL";
761                 SELECT  $distinct_list
762                   FROM  $u_table AS users $card
763                         JOIN $descendants d ON (d.id = users.home_ou)
764                         $select
765                         $opt_in_join
766                         $clone_select
767             $penalty_join
768                   WHERE users.deleted = FALSE
769                         $inactive
770                         $opt_in_where
771                   GROUP BY $group_list
772                   ORDER BY $order_by
773                   LIMIT $limit
774         SQL
775
776         return actor::user->db_Main->selectcol_arrayref($select, {Columns=>[scalar(@$sort)]}, map {lc($_)} (@usrv,@phonev,@identv,@namev,@addrv));
777 }
778 __PACKAGE__->register_method(
779         api_name        => 'open-ils.storage.actor.user.crazy_search',
780         api_level       => 1,
781         method          => 'patron_search',
782 );
783
784 sub org_unit_list {
785         my $self = shift;
786         my $client = shift;
787
788         my $select =<<" SQL";
789         SELECT  *
790           FROM  actor.org_unit
791           ORDER BY CASE WHEN parent_ou IS NULL THEN 0 ELSE 1 END, name;
792         SQL
793
794         my $sth = actor::org_unit->db_Main->prepare_cached($select);
795         $sth->execute;
796
797         $client->respond( $_->to_fieldmapper ) for ( map { actor::org_unit->construct($_) } $sth->fetchall_hash );
798
799         return undef;
800 }
801 __PACKAGE__->register_method(
802         api_name        => 'open-ils.storage.direct.actor.org_unit.retrieve.all',
803         api_level       => 1,
804         stream          => 1,
805         method          => 'org_unit_list',
806 );
807
808 sub org_unit_type_list {
809         my $self = shift;
810         my $client = shift;
811
812         my $select =<<" SQL";
813         SELECT  *
814           FROM  actor.org_unit_type
815           ORDER BY depth, name;
816         SQL
817
818         my $sth = actor::org_unit_type->db_Main->prepare_cached($select);
819         $sth->execute;
820
821         $client->respond( $_->to_fieldmapper ) for ( map { actor::org_unit_type->construct($_) } $sth->fetchall_hash );
822
823         return undef;
824 }
825 __PACKAGE__->register_method(
826         api_name        => 'open-ils.storage.direct.actor.org_unit_type.retrieve.all',
827         api_level       => 1,
828         stream          => 1,
829         method          => 'org_unit_type_list',
830 );
831
832 sub org_unit_full_path {
833         my $self = shift;
834         my $client = shift;
835         my @binds = @_;
836
837         return undef unless (@binds);
838
839         my $func = 'actor.org_unit_full_path(?)';
840         $func = 'actor.org_unit_full_path(?,?)' if (@binds > 1);
841
842         my $sth = actor::org_unit->db_Main->prepare_cached("SELECT * FROM $func");
843         $sth->execute(@binds);
844
845         $client->respond( $_->to_fieldmapper ) for ( map { actor::org_unit->construct($_) } $sth->fetchall_hash );
846
847         return undef;
848 }
849 __PACKAGE__->register_method(
850         api_name        => 'open-ils.storage.actor.org_unit.full_path',
851         api_level       => 1,
852         stream          => 1,
853         method          => 'org_unit_full_path',
854 );
855
856 sub org_unit_ancestors {
857         my $self = shift;
858         my $client = shift;
859         my $id = shift;
860
861         return undef unless ($id);
862
863         my $func = 'actor.org_unit_ancestors(?)';
864
865         my $sth = actor::org_unit->db_Main->prepare_cached(<<"  SQL");
866                 SELECT  f.*
867                   FROM  $func f
868                         JOIN actor.org_unit_type t ON (f.ou_type = t.id)
869                   ORDER BY t.depth, f.name;
870         SQL
871         $sth->execute(''.$id);
872
873         $client->respond( $_->to_fieldmapper ) for ( map { actor::org_unit->construct($_) } $sth->fetchall_hash );
874
875         return undef;
876 }
877 __PACKAGE__->register_method(
878         api_name        => 'open-ils.storage.actor.org_unit.ancestors',
879         api_level       => 1,
880         stream          => 1,
881         method          => 'org_unit_ancestors',
882 );
883
884 sub org_unit_descendants {
885         my $self = shift;
886         my $client = shift;
887         my $id = shift;
888         my $depth = shift;
889
890         return undef unless ($id);
891
892         my $func = 'actor.org_unit_descendants(?)';
893         if (defined $depth) {
894                 $func = 'actor.org_unit_descendants(?,?)';
895         }
896
897         my $sth = actor::org_unit->db_Main->prepare_cached("SELECT * FROM $func");
898         $sth->execute(''.$id, ''.$depth) if (defined $depth);
899         $sth->execute(''.$id) unless (defined $depth);
900
901         $client->respond( $_->to_fieldmapper ) for ( map { actor::org_unit->construct($_) } $sth->fetchall_hash );
902
903         return undef;
904 }
905 __PACKAGE__->register_method(
906         api_name        => 'open-ils.storage.actor.org_unit.descendants',
907         api_level       => 1,
908         stream          => 1,
909         method          => 'org_unit_descendants',
910 );
911
912 sub fleshed_actor_stat_cat {
913         my $self = shift;
914         my $client = shift;
915         my @list = @_;
916         
917         @list = ($list[0]) unless ($self->api_name =~ /batch/o);
918
919         for my $sc (@list) {
920                 my $cat = actor::stat_cat->retrieve($sc);
921                 next unless ($cat);
922
923                 my $sc_fm = $cat->to_fieldmapper;
924                 $sc_fm->entries( [ map { $_->to_fieldmapper } $cat->entries ] );
925
926                 $client->respond( $sc_fm );
927
928         }
929
930         return undef;
931 }
932 __PACKAGE__->register_method(
933         api_name        => 'open-ils.storage.fleshed.actor.stat_cat.retrieve',
934         api_level       => 1,
935         argc            => 1,
936         method          => 'fleshed_actor_stat_cat',
937 );
938
939 __PACKAGE__->register_method(
940         api_name        => 'open-ils.storage.fleshed.actor.stat_cat.retrieve.batch',
941         api_level       => 1,
942         argc            => 1,
943         stream          => 1,
944         method          => 'fleshed_actor_stat_cat',
945 );
946
947 #XXX Fix stored proc calls
948 sub ranged_actor_stat_cat_all {
949         my $self = shift;
950         my $client = shift;
951         my $ou = ''.shift();
952         
953         return undef unless ($ou);
954         my $s_table = actor::stat_cat->table;
955
956         my $select = <<"        SQL";
957                 SELECT  s.*
958                   FROM  $s_table s
959                         JOIN actor.org_unit_full_path(?) p ON (p.id = s.owner)
960                   ORDER BY name
961         SQL
962
963         $fleshed = 0;
964         $fleshed = 1 if ($self->api_name =~ /fleshed/o);
965
966         my $sth = actor::stat_cat->db_Main->prepare_cached($select);
967         $sth->execute($ou);
968
969         for my $sc ( map { actor::stat_cat->construct($_) } $sth->fetchall_hash ) {
970                 my $sc_fm = $sc->to_fieldmapper;
971                 $sc_fm->entries(
972                         [ $self->method_lookup( 'open-ils.storage.ranged.actor.stat_cat_entry.search.stat_cat' )->run($ou,$sc->id) ]
973                 ) if ($fleshed);
974                 $client->respond( $sc_fm );
975         }
976
977         return undef;
978 }
979 __PACKAGE__->register_method(
980         api_name        => 'open-ils.storage.ranged.fleshed.actor.stat_cat.all',
981         api_level       => 1,
982         argc            => 1,
983         stream          => 1,
984         method          => 'ranged_actor_stat_cat_all',
985 );
986
987 __PACKAGE__->register_method(
988         api_name        => 'open-ils.storage.ranged.actor.stat_cat.all',
989         api_level       => 1,
990         argc            => 1,
991         stream          => 1,
992         method          => 'ranged_actor_stat_cat_all',
993 );
994
995 #XXX Fix stored proc calls
996 sub ranged_actor_stat_cat_entry {
997         my $self = shift;
998         my $client = shift;
999         my $ou = ''.shift();
1000         my $sc = ''.shift();
1001         
1002         return undef unless ($ou);
1003         my $s_table = actor::stat_cat_entry->table;
1004
1005         my $select = <<"        SQL";
1006                 SELECT  s.*
1007                   FROM  $s_table s
1008                         JOIN actor.org_unit_full_path(?) p ON (p.id = s.owner)
1009                   WHERE stat_cat = ?
1010                   ORDER BY name
1011         SQL
1012
1013         my $sth = actor::stat_cat->db_Main->prepare_cached($select);
1014         $sth->execute($ou,$sc);
1015
1016         for my $sce ( map { actor::stat_cat_entry->construct($_) } $sth->fetchall_hash ) {
1017                 $client->respond( $sce->to_fieldmapper );
1018         }
1019
1020         return undef;
1021 }
1022 __PACKAGE__->register_method(
1023         api_name        => 'open-ils.storage.ranged.actor.stat_cat_entry.search.stat_cat',
1024         api_level       => 1,
1025         stream          => 1,
1026         method          => 'ranged_actor_stat_cat_entry',
1027 );
1028
1029
1030 1;