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