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