]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Collections.pm
QueryParser Driver: Much work
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Collections.pm
1 package OpenILS::Application::Collections;
2 use strict; use warnings;
3 use OpenSRF::EX qw(:try);
4 use OpenILS::Application::AppUtils;
5 use OpenSRF::Utils::Logger qw(:logger);
6 use OpenSRF::Utils qw/:datetime/;
7 use OpenILS::Application;
8 use OpenILS::Utils::Fieldmapper;
9 use base 'OpenILS::Application';
10 use OpenILS::Utils::CStoreEditor qw/:funcs/;
11 use OpenILS::Event;
12 use OpenILS::Const qw/:const/;
13 my $U = "OpenILS::Application::AppUtils";
14 use XML::LibXML;
15 use Scalar::Util 'blessed';
16 use File::Spec;
17 use File::Copy;
18 use File::Path;
19
20
21 # --------------------------------------------------------------
22 # Loads the config info
23 # --------------------------------------------------------------
24 sub initialize { return 1; }
25
26 __PACKAGE__->register_method(
27     method => 'user_from_bc',
28     api_name => 'open-ils.collections.user_id_from_barcode',
29 );
30
31 sub user_from_bc {
32     my( $self, $conn, $auth, $bc ) = @_;
33     my $e = new_editor(authtoken=>$auth);
34     return $e->event unless $e->checkauth;
35     return $e->event unless $e->allowed('VIEW_USER');
36     my $card = $e->search_actor_card({barcode=>$bc})->[0]
37         or return $e->event;
38     my $user = $e->retrieve_actor_user($card->usr)
39         or return $e->event;
40     return $user->id;
41 }
42
43
44 __PACKAGE__->register_method(
45     method    => 'users_of_interest',
46     api_name  => 'open-ils.collections.users_of_interest.retrieve',
47     api_level => 1,
48     argc      => 4,
49     stream    => 1,
50     signature => {
51         desc     => q/
52             Returns an array of user information objects that the system
53             based on the search criteria provided.  If the total fines
54             a user owes reaches or exceeds "fine_level" on or befre "age"
55             and the fines were created at "location", the user will be
56             included in the return set/,
57
58         params   => [
59             {    name => 'auth',
60                 desc => 'The authentication token',
61                 type => 'string' },
62
63             {    name => 'age',
64                 desc => q/Number of days back to check/,
65                 type => q/number/,
66             },
67
68             {    name => 'fine_level',
69                 desc => q/The fine threshold at which users will be included in the search results /,
70                 type => q/number/,
71             },
72             {    name => 'location',
73                 desc => q/The short-name of the orginization unit (library) at which the fines were created.
74                             If a selected location has 'child' locations (e.g. a library region), the
75                             child locations will be included in the search/,
76                 type => q/string/,
77             },
78         ],
79
80           'return' => {
81             desc        => q/An array of user information objects.
82                         usr : Array of user information objects containing id, dob, profile, and groups
83                         threshold_amount : The total amount the patron owes that is at least as old
84                             as the fine "age" and whose transaction was created at the searched location
85                         last_pertinent_billing : The time of the last billing that relates to this query
86                         /,
87             type        => 'array',
88             example    => {
89                 usr    => {
90                     id            => 'id',
91                     dob        => '1970-01-01',
92                     profile    => 'Patron',
93                     groups    => [ 'Patron', 'Staff' ],
94                 },
95                 threshold_amount => 99,
96             }
97         }
98     }
99 );
100
101
102 sub users_of_interest {
103     my( $self, $conn, $auth, $age, $fine_level, $location ) = @_;
104
105     return OpenILS::Event->new('BAD_PARAMS')
106         unless ($auth and $age and $location);
107
108     my $e = new_editor(authtoken => $auth);
109     return $e->event unless $e->checkauth;
110
111     my $org = $e->search_actor_org_unit({shortname => $location})
112         or return $e->event; $org = $org->[0];
113
114     # they need global perms to view users so no org is provided
115     return $e->event unless $e->allowed('VIEW_USER');
116
117     my $data = [];
118
119     my $ses = OpenSRF::AppSession->create('open-ils.storage');
120
121     my $start = time;
122     my $req = $ses->request(
123         'open-ils.storage.money.collections.users_of_interest',
124         $age, $fine_level, $location);
125
126     # let the client know we're still here
127     $conn->status( new OpenSRF::DomainObject::oilsContinueStatus );
128
129     return process_users_of_interest_results(
130         $self, $conn, $e, $req, $start, $age, $fine_level, $location);
131 }
132
133
134 __PACKAGE__->register_method(
135     method    => 'users_of_interest_warning_penalty',
136     api_name  => 'open-ils.collections.users_of_interest.warning_penalty.retrieve',
137     api_level => 1,
138     argc      => 4,
139     stream    => 1,
140     signature => {
141         desc     => q/
142             Returns an array of user information objects for users that have the
143             PATRON_EXCEEDS_COLLECTIONS_WARNING penalty applied,
144             based on the search criteria provided./,
145
146         params   => [
147             {    name => 'auth',
148                 desc => 'The authentication token',
149                 type => 'string'
150             }, {
151                 name => 'location',
152                 desc => q/The short-name of the orginization unit (library) at which the penalty is applied.
153                             If a selected location has 'child' locations (e.g. a library region), the
154                             child locations will be included in the search/,
155                 type => q/string/,
156             }, {
157                 name => 'min_age',
158                 desc => q/Optional.  Minimum age of the penalty application/,
159                 type => q/interval, e.g "30 days"/,
160             }, {
161                 name => 'max_age',
162                 desc => q/Optional.  Maximum age of the penalty application/,
163                 type => q/interval, e.g "90 days"/,
164             }
165         ],
166
167           'return' => {
168             desc        => q/An array of user information objects.
169                         usr : Array of user information objects containing id, dob, profile, and groups
170                         threshold_amount : The total amount the patron owes that is at least as old
171                             as the fine "age" and whose transaction was created at the searched location
172                         last_pertinent_billing : The time of the last billing that relates to this query
173                         /,
174             type        => 'array',
175             example    => {
176                 usr    => {
177                     id            => 'id',
178                     dob        => '1970-01-01',
179                     profile    => 'Patron',
180                     groups    => [ 'Patron', 'Staff' ],
181                 },
182                 threshold_amount => 99, # TODO: still needed?
183             }
184         }
185     }
186 );
187
188
189
190 sub users_of_interest_warning_penalty {
191     my( $self, $conn, $auth, $location, $min_age, $max_age ) = @_;
192
193     return OpenILS::Event->new('BAD_PARAMS') unless ($auth and $location);
194
195     my $e = new_editor(authtoken => $auth);
196     return $e->event unless $e->checkauth;
197
198     my $org = $e->search_actor_org_unit({shortname => $location})
199         or return $e->event; $org = $org->[0];
200
201     # they need global perms to view users so no org is provided
202     return $e->event unless $e->allowed('VIEW_USER');
203
204     my $org_ids = $e->json_query({from => ['actor.org_unit_full_path', $org->id]});
205
206     my $ses = OpenSRF::AppSession->create('open-ils.cstore');
207
208     # max age == oldest
209     my $max_set_date = DateTime->now->subtract(seconds =>
210         interval_to_seconds($max_age))->strftime( '%F %T%z' ) if $max_age;
211     my $min_set_date = DateTime->now->subtract(seconds =>
212         interval_to_seconds($min_age))->strftime( '%F %T%z' ) if $min_age;
213
214     my $start = time;
215     my $query = {
216         select => {ausp => ['usr']},
217         from => {
218             ausp => {
219                 au => {
220                     join => {
221                         aus => {
222                             type => 'left',
223                             filter => {name => 'circ.collections.exempt'}
224                         },
225                         mct => {
226                             type => 'left',
227                             filter => {
228                                 location => [ map {$_->{id}} @$org_ids ]
229                             }
230                         }
231                     }
232                 }
233             }
234         },
235         where => {
236             '+ausp' => {
237                 standing_penalty => 4, # PATRON_EXCEEDS_COLLECTIONS_WARNING
238                 org_unit => [ map {$_->{id}} @$org_ids ],
239                 '-or' => [
240                     {stop_date => undef},
241                     {stop_date => {'>' => 'now'}}
242                 ]
243             },
244             # We are only interested in users that do not have the
245             # circ.collections.exempt setting applied
246             '+aus' => {value => undef},
247             # and we're only interested in users that are not in the
248             # collections tracker table
249             '+mct' => {id => undef}
250         }
251     };
252
253     $query->{where}->{'-and'} = [] if $max_set_date or $min_set_date;
254     push(@{$query->{where}->{'-and'}}, {set_date => {'>' => $max_set_date}}) if $max_set_date;
255     push(@{$query->{where}->{'-and'}}, {set_date => {'<' => $min_set_date}}) if $min_set_date;
256
257     my $req = $ses->request('open-ils.cstore.json_query', $query);
258
259     # let the client know we're still here
260     $conn->status( new OpenSRF::DomainObject::oilsContinueStatus );
261
262     return process_users_of_interest_results(
263         $self, $conn, $e, $req, $start, $min_age, '', $location, $max_age);
264 }
265
266
267
268
269 sub process_users_of_interest_results {
270     my($self, $conn, $e, $req, $starttime, @params) = @_;
271
272    my $total;
273    while( my $resp = $req->recv(timeout => 7200) ) {
274
275         return $req->failed if $req->failed;
276         my $hash = $resp->content;
277         next unless $hash;
278
279         unless($total) {
280             $total = time - $starttime;
281             $logger->info("collections: request (@params) took $total seconds");
282         }
283
284         my $u = $e->retrieve_actor_user(
285             [
286                 $hash->{usr},
287                 {
288                     flesh                => 1,
289                     flesh_fields    => {au => ["groups","profile", "card"]},
290                 }
291             ]
292         ) or return $e->event;
293
294         $hash->{usr} = {
295             id            => $u->id,
296             dob        => $u->dob,
297             profile    => $u->profile->name,
298             barcode    => $u->card->barcode,
299             groups    => [ map { $_->name } @{$u->groups} ],
300         };
301
302         $conn->respond($hash);
303     }
304
305     return undef;
306 }
307
308
309 __PACKAGE__->register_method(
310     method    => 'users_owing_money',
311     api_name  => 'open-ils.collections.users_owing_money.retrieve',
312     api_level => 1,
313     argc      => 5,
314     stream    => 1,
315     signature => {
316         desc     => q/
317             Returns an array of users that owe money during
318             the given time frame at the location (or child locations)
319             provided/,
320
321         params   => [
322             {    name => 'auth',
323                 desc => 'The authentication token',
324                 type => 'string' },
325
326             {    name => 'start_date',
327                 desc => 'The start of the time interval to check',
328                 type => q/string (ISO 8601 timestamp.  E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
329             },
330
331             {    name => 'end_date',
332                 desc => q/Then end date of the time interval to check/,
333                 type => q/string (ISO 8601 timestamp.  E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
334             },
335             {    name => 'fine_level',
336                 desc => q/The fine threshold at which users will be included in the search results /,
337                 type => q/number/,
338             },
339             {    name => 'locations',
340                 desc => q/  A list of one or more org-unit short names.
341                             If a selected location has 'child' locations (e.g. a library region), the
342                             child locations will be included in the search/,
343                 type => q'string',
344             },
345         ],
346           'return' => {
347             desc        => q/An array of user information objects/,
348             type        => 'array',
349         }
350     }
351 );
352
353
354 sub users_owing_money {
355     my( $self, $conn, $auth, $start_date, $end_date, $fine_level, @locations ) = @_;
356
357     return OpenILS::Event->new('BAD_PARAMS')
358         unless ($auth and $start_date and $end_date and @locations);
359
360     my $e = new_editor(authtoken => $auth);
361     return $e->event unless $e->checkauth;
362
363     # they need global perms to view users so no org is provided
364     return $e->event unless $e->allowed('VIEW_USER');
365
366     my $data = [];
367
368     my $ses = OpenSRF::AppSession->create('open-ils.storage');
369
370     my $start = time;
371     my $req = $ses->request(
372         'open-ils.storage.money.collections.users_owing_money',
373         $start_date, $end_date, $fine_level, @locations);
374
375     # let the client know we're still here
376     $conn->status( new OpenSRF::DomainObject::oilsContinueStatus );
377
378     return process_users_of_interest_results(
379         $self, $conn, $e, $req, $start, $start_date, $end_date, $fine_level, @locations);
380 }
381
382
383
384 __PACKAGE__->register_method(
385     method    => 'users_with_activity',
386     api_name  => 'open-ils.collections.users_with_activity.retrieve',
387     api_level => 1,
388     argc      => 4,
389     stream    => 1,
390     signature => {
391         desc     => q/
392             Returns an array of users that are already in collections
393             and had any type of billing or payment activity within
394             the given time frame at the location (or child locations)
395             provided/,
396
397         params   => [
398             {    name => 'auth',
399                 desc => 'The authentication token',
400                 type => 'string' },
401
402             {    name => 'start_date',
403                 desc => 'The start of the time interval to check',
404                 type => q/string (ISO 8601 timestamp.  E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
405             },
406
407             {    name => 'end_date',
408                 desc => q/Then end date of the time interval to check/,
409                 type => q/string (ISO 8601 timestamp.  E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
410             },
411             {    name => 'location',
412                 desc => q/The short-name of the orginization unit (library) at which the activity occurred.
413                             If a selected location has 'child' locations (e.g. a library region), the
414                             child locations will be included in the search/,
415                 type => q'string',
416             },
417         ],
418
419           'return' => {
420             desc        => q/An array of user information objects/,
421             type        => 'array',
422         }
423     }
424 );
425
426 sub users_with_activity {
427     my( $self, $conn, $auth, $start_date, $end_date, $location ) = @_;
428     return OpenILS::Event->new('BAD_PARAMS')
429         unless ($auth and $start_date and $end_date and $location);
430
431     my $e = new_editor(authtoken => $auth);
432     return $e->event unless $e->checkauth;
433
434     my $org = $e->search_actor_org_unit({shortname => $location})
435         or return $e->event; $org = $org->[0];
436     return $e->event unless $e->allowed('VIEW_USER', $org->id);
437
438     my $ses = OpenSRF::AppSession->create('open-ils.storage');
439
440     my $start = time;
441     my $req = $ses->request(
442         'open-ils.storage.money.collections.users_with_activity.atomic',
443         $start_date, $end_date, $location);
444
445     $conn->status( new OpenSRF::DomainObject::oilsContinueStatus );
446
447     my $total;
448     while( my $resp = $req->recv(timeout => 7200) ) {
449
450         unless($total) {
451             $total = time - $start;
452             $logger->info("collections: users_with_activity search ".
453                 "($start_date, $end_date, $location) took $total seconds");
454         }
455
456         return $req->failed if $req->failed;
457         $conn->respond($resp->content);
458    }
459
460     return undef;
461 }
462
463
464
465 __PACKAGE__->register_method(
466     method    => 'put_into_collections',
467     api_name  => 'open-ils.collections.put_into_collections',
468     api_level => 1,
469     argc      => 3,
470     signature => {
471         desc     => q/
472             Marks a user as being "in collections" at a given location
473             /,
474
475         params   => [
476             {    name => 'auth',
477                 desc => 'The authentication token',
478                 type => 'string' },
479
480             {    name => 'user_id',
481                 desc => 'The id of the user to plact into collections',
482                 type => 'number',
483             },
484
485             {    name => 'location',
486                 desc => q/The short-name of the orginization unit (library)
487                     for which the user is being placed in collections/,
488                 type => q'string',
489             },
490             {    name => 'fee_amount',
491                 desc => q/
492                     The amount of money that a patron should be fined.
493                     If this field is empty, no fine is created.
494                 /,
495                 type => 'string',
496             },
497             {    name => 'fee_note',
498                 desc => q/
499                     Custom note that is added to the the billing.
500                     This field is not required.
501                     Note: fee_note is not the billing_type.  Billing_type type is
502                     decided by the system. (e.g. "fee for collections").
503                     fee_note is purely used for any additional needed information
504                     and is only visible to staff.
505                 /,
506                 type => 'string',
507             },
508         ],
509
510           'return' => {
511             desc        => q/A SUCCESS event on success, error event on failure/,
512             type        => 'object',
513         }
514     }
515 );
516 sub put_into_collections {
517     my( $self, $conn, $auth, $user_id, $location, $fee_amount, $fee_note ) = @_;
518
519     return OpenILS::Event->new('BAD_PARAMS')
520         unless ($auth and $user_id and $location);
521
522     my $e = new_editor(authtoken => $auth, xact =>1);
523     return $e->event unless $e->checkauth;
524
525     my $org = $e->search_actor_org_unit({shortname => $location});
526     return $e->event unless $org = $org->[0];
527     return $e->event unless $e->allowed('money.collections_tracker.create', $org->id);
528
529     my $existing = $e->search_money_collections_tracker(
530         {
531             location        => $org->id,
532             usr            => $user_id,
533             collector    => $e->requestor->id
534         },
535         {idlist => 1}
536     );
537
538     return OpenILS::Event->new('MONEY_COLLECTIONS_TRACKER_EXISTS') if @$existing;
539
540     $logger->info("collect: user ".$e->requestor->id.
541         " putting user $user_id into collections for $location");
542
543     my $tracker = Fieldmapper::money::collections_tracker->new;
544
545     $tracker->usr($user_id);
546     $tracker->collector($e->requestor->id);
547     $tracker->location($org->id);
548     $tracker->enter_time('now');
549
550     $e->create_money_collections_tracker($tracker)
551         or return $e->event;
552
553     if( $fee_amount ) {
554         my $evt = add_collections_fee($e, $user_id, $org, $fee_amount, $fee_note );
555         return $evt if $evt;
556     }
557
558     $e->commit;
559
560     my $pen = Fieldmapper::actor::user_standing_penalty->new;
561     $pen->org_unit($org->id);
562     $pen->usr($user_id);
563     $pen->standing_penalty(30); # PATRON_IN_COLLECTIONS
564     $pen->staff($e->requestor->id);
565     $pen->note($fee_note) if $fee_note;
566     $U->simplereq('open-ils.actor', 'open-ils.actor.user.penalty.apply', $auth, $pen);
567
568     return OpenILS::Event->new('SUCCESS');
569 }
570
571 sub add_collections_fee {
572     my( $e, $patron_id, $org, $fee_amount, $fee_note ) = @_;
573
574     $fee_note ||= "";
575
576     $logger->info("collect: adding fee to user $patron_id : $fee_amount : $fee_note");
577
578     my $xact = Fieldmapper::money::grocery->new;
579     $xact->usr($patron_id);
580     $xact->xact_start('now');
581     $xact->billing_location($org->id);
582
583     $xact = $e->create_money_grocery($xact) or return $e->event;
584
585     my $bill = Fieldmapper::money::billing->new;
586     $bill->note($fee_note);
587     $bill->xact($xact->id);
588     $bill->btype(2);
589     $bill->billing_type(OILS_BILLING_TYPE_COLLECTION_FEE);
590     $bill->amount($fee_amount);
591
592     $e->create_money_billing($bill) or return $e->event;
593     return undef;
594 }
595
596
597
598
599 __PACKAGE__->register_method(
600     method        => 'remove_from_collections',
601     api_name        => 'open-ils.collections.remove_from_collections',
602     signature    => q/
603         Returns the users that are currently in collections and
604         had activity during the provided interval.  Dates are inclusive.
605         @param start_date The beginning of the activity interval
606         @param end_date The end of the activity interval
607         @param location The location at which the fines were created
608     /
609 );
610
611
612 __PACKAGE__->register_method(
613     method    => 'remove_from_collections',
614     api_name  => 'open-ils.collections.remove_from_collections',
615     api_level => 1,
616     argc      => 3,
617     signature => {
618         desc     => q/
619             Removes a user from the collections table for the given location
620             /,
621
622         params   => [
623             {    name => 'auth',
624                 desc => 'The authentication token',
625                 type => 'string' },
626
627             {    name => 'user_id',
628                 desc => 'The id of the user to plact into collections',
629                 type => 'number',
630             },
631
632             {    name => 'location',
633                 desc => q/The short-name of the orginization unit (library)
634                     for which the user is being removed from collections/,
635                 type => q'string',
636             },
637         ],
638
639           'return' => {
640             desc        => q/A SUCCESS event on success, error event on failure/,
641             type        => 'object',
642         }
643     }
644 );
645
646 sub remove_from_collections {
647     my( $self, $conn, $auth, $user_id, $location ) = @_;
648
649     return OpenILS::Event->new('BAD_PARAMS')
650         unless ($auth and $user_id and $location);
651
652     my $e = new_editor(authtoken => $auth, xact=>1);
653     return $e->event unless $e->checkauth;
654
655     my $org = $e->search_actor_org_unit({shortname => $location})
656         or return $e->event; $org = $org->[0];
657     return $e->event unless $e->allowed('money.collections_tracker.delete', $org->id);
658
659     my $tracker = $e->search_money_collections_tracker(
660         { usr => $user_id, location => $org->id })
661         or return $e->event;
662
663     $e->delete_money_collections_tracker($tracker->[0])
664         or return $e->event;
665
666     $e->commit;
667     return OpenILS::Event->new('SUCCESS');
668 }
669
670
671 #__PACKAGE__->register_method(
672 #    method        => 'transaction_details',
673 #    api_name        => 'open-ils.collections.user_transaction_details.retrieve',
674 #    signature    => q/
675 #    /
676 #);
677
678
679 __PACKAGE__->register_method(
680     method    => 'transaction_details',
681     api_name  => 'open-ils.collections.user_transaction_details.retrieve',
682     api_level => 1,
683     argc      => 5,
684     signature => {
685         desc     => q/
686             Returns a list of fleshed user objects with transaction details
687             /,
688
689         params   => [
690             {    name => 'auth',
691                 desc => 'The authentication token',
692                 type => 'string' },
693
694             {    name => 'start_date',
695                 desc => 'The start of the time interval to check',
696                 type => q/string (ISO 8601 timestamp.  E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
697             },
698
699             {    name => 'end_date',
700                 desc => q/Then end date of the time interval to check/,
701                 type => q/string (ISO 8601 timestamp.  E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
702             },
703             {    name => 'location',
704                 desc => q/The short-name of the orginization unit (library) at which the activity occurred.
705                             If a selected location has 'child' locations (e.g. a library region), the
706                             child locations will be included in the search/,
707                 type => q'string',
708             },
709             {
710                 name => 'user_list',
711                 desc => 'An array of user ids',
712                 type => 'array',
713             },
714         ],
715
716           'return' => {
717             desc        => q/A list of objects.  Object keys include:
718                 usr :
719                 transactions : An object with keys :
720                     circulations : Fleshed circulation objects
721                     grocery : Fleshed 'grocery' transaction objects
722                 /,
723             type        => 'object'
724         }
725     }
726 );
727
728 sub transaction_details {
729     my( $self, $conn, $auth, $start_date, $end_date, $location, $user_list ) = @_;
730
731     return OpenILS::Event->new('BAD_PARAMS')
732         unless ($auth and $start_date and $end_date and $location and $user_list);
733
734     my $e = new_editor(authtoken => $auth);
735     return $e->event unless $e->checkauth;
736
737     # they need global perms to view users so no org is provided
738     return $e->event unless $e->allowed('VIEW_USER');
739
740     my $org = $e->search_actor_org_unit({shortname => $location})
741         or return $e->event; $org = $org->[0];
742
743     # get a reference to the org inside of the tree
744     $org = $U->find_org($U->get_org_tree(), $org->id);
745
746     my @data;
747     for my $uid (@$user_list) {
748         my $blob = {};
749
750         $blob->{usr} = $e->retrieve_actor_user(
751             [
752                 $uid,
753              {
754                 "flesh"        => 1,
755                 "flesh_fields" =>  {
756                    "au" => [
757                       "cards",
758                       "card",
759                       "standing_penalties",
760                       "addresses",
761                       "billing_address",
762                       "mailing_address",
763                       "stat_cat_entries"
764                    ]
765                 }
766              }
767             ]
768         );
769
770         $blob->{transactions} = {
771             circulations    =>
772                 fetch_circ_xacts($e, $uid, $org, $start_date, $end_date),
773             grocery            =>
774                 fetch_grocery_xacts($e, $uid, $org, $start_date, $end_date),
775             reservations    =>
776                 fetch_reservation_xacts($e, $uid, $org, $start_date, $end_date)
777         };
778
779         # for each transaction, flesh the workstatoin on any attached payment
780         # and make the payment object a real object (e.g. cash payment),
781         # not just a generic payment object
782         for my $xact (
783             @{$blob->{transactions}->{circulations}},
784             @{$blob->{transactions}->{reservations}},
785             @{$blob->{transactions}->{grocery}} ) {
786
787             my $ps;
788             if( $ps = $xact->payments and @$ps ) {
789                 my @fleshed; my $evt;
790                 for my $p (@$ps) {
791                     ($p, $evt) = flesh_payment($e,$p);
792                     return $evt if $evt;
793                     push(@fleshed, $p);
794                 }
795                 $xact->payments(\@fleshed);
796             }
797         }
798
799         push( @data, $blob );
800     }
801
802     return \@data;
803 }
804
805 __PACKAGE__->register_method(
806     method    => 'user_balance_summary',
807     api_name  => 'open-ils.collections.user_balance_summary.generate',
808     api_level => 1,
809     stream    => 1,
810     argc      => 2,
811     signature => {
812         desc     => q/Collect balance information for users in collections.  By default,
813                         only the total balance owed is calculated.  Use the "include_xacts"
814                         param to include per-transaction summaries as well./,
815         params   => [
816             {   name => 'auth',
817                 desc => 'The authentication token',
818                 type => 'string' },
819             {   name => 'args',
820                 desc => q/
821                     Hash of API arguments.  Options include:
822                     location   -- org unit shortname
823                     start_date -- ISO 8601 date. limit to patrons added to collections on or after this date (optional).
824                     end_date   -- ISO 8601 date. limit to patrons added to collections on or before this date (optional).
825                     user_id    -- retrieve information only for this user (takes preference over
826                         start and end_date).  May be a single ID or list of IDs. (optional).
827                     include_xacts -- If true, include a summary object per transaction in addition to the full balance owed
828                 /,
829                 type => q/hash/
830             },
831         ],
832         'return' => {
833             desc => q/
834                 The file name prefix of the file to be created.
835                 The file name format will be:
836                 user_balance_YYYY-MM-DD_${location}_${start_date}_${end_date}_${user_id}.[tmp|xml]
837                 Optional params not provided by the caller will not be part of the file name.
838                 Examples:
839                     user_balance_BR1_2012-05-25_2012-01-01_2012-12-31 # start and end dates
840                     user_balance_BR2_2012-05-25_153244 # user id only.
841                 In-process files will have a .tmp suffix
842                 Completed files will have a .xml suffix
843             /,
844             type => 'string'
845         }
846     }
847 );
848
849 sub user_balance_summary {
850     my ($self, $client, $auth, $args) = @_;
851
852     my $location = $$args{location};
853     my $start_date = $$args{start_date};
854     my $end_date = $$args{end_date};
855     my $user_id = $$args{user_id};
856
857     return OpenILS::Event->new('BAD_PARAMS')
858         unless $auth and $location and
859         ($start_date or $end_date or $user_id);
860
861     my $e = new_editor(authtoken => $auth);
862     return $e->event unless $e->checkauth;
863
864     my $org = $e->search_actor_org_unit({shortname => $location})->[0]
865         or return $e->event;
866
867     # they need global perms to view users so no org is provided
868     return $e->event unless $e->allowed('VIEW_USER', $org->id);
869
870     my $org_list = $U->get_org_descendants($org->id);
871
872     my ($evt, $file_prefix, $file_name, $FILE) = setup_batch_file('user_balance', $args);
873
874     $client->respond_complete($evt || $file_prefix);
875
876     my @user_list;
877
878     if ($user_id) {
879         @user_list = (ref $user_id eq 'ARRAY') ? @$user_id : ($user_id);
880
881     } else {
882         # collect the users from the tracker table based on the provided filters
883
884         my $query = {
885             select => {mct => ['usr']},
886             from => 'mct',
887             where => {location => $org_list}
888         };
889
890         $query->{where}->{enter_time} = {'>=' => $start_date};
891         $query->{where}->{enter_time} = {'<=' => $end_date};
892         my $users = $e->json_query($query);
893         @user_list = map {$_->{usr}} @$users;
894     }
895
896     print $FILE "<Collections>\n"; # append to the document as we have data
897
898     for my $user_id (@user_list) {
899         my $user_doc = XML::LibXML::Document->new;
900         my $root = $user_doc->createElement('User');
901         $user_doc->setDocumentElement($root);
902
903         my $user = $e->retrieve_actor_user([
904             $user_id, {
905             flesh        => 1,
906             flesh_fields => {
907                 au => [
908                     'card',
909                     'cards',
910                     'standing_penalties',
911                     'addresses',
912                     'billing_address',
913                     'mailing_address',
914                     'stat_cat_entries'
915                 ]
916             }}
917         ]);
918
919         my $au_doc = $user->toXML({no_virt => 1, skip_fields => {au => ['passwd']}});
920         my $au_node = $au_doc->documentElement;
921         $user_doc->adoptNode($au_node);
922         $root->appendChild($au_node);
923
924         my $circ_ids = $e->search_action_circulation(
925             {usr => $user_id, circ_lib => $org_list, xact_finish => undef},
926             {idlist => 1}
927         );
928
929         my $groc_ids = $e->search_money_grocery(
930             {usr => $user_id, billing_location => $org_list, xact_finish => undef},
931             {idlist => 1}
932         );
933
934         my $res_ids = $e->search_booking_reservation(
935             {usr => $user_id, pickup_lib => $org_list, xact_finish => undef},
936             {idlist => 1}
937         );
938
939         # get the sum owed an all transactions
940         my $balance = $e->json_query({
941             select => {mbts => [
942                 {   column => 'balance_owed',
943                     transform => 'sum',
944                     aggregate => 1
945                 }
946             ]},
947             from => 'mbts',
948             where => {id => [@$circ_ids, @$groc_ids, @$res_ids]}
949         })->[0];
950
951         $balance = $balance ? $balance->{balance_owed} : '0';
952
953         my $xacts_node = $user_doc->createElement('Transactions');
954         my $balance_node = $user_doc->createElement('BalanceOwed');
955         $balance_node->appendChild($user_doc->createTextNode($balance));
956         $xacts_node->appendChild($balance_node);
957         $root->appendChild($xacts_node);
958
959         if ($$args{include_xacts}) {
960             my $xacts = $e->search_money_billable_transaction_summary(
961                 {id => [@$circ_ids, @$groc_ids, @$res_ids]},
962                 {substream => 1}
963             );
964
965             for my $xact (@$xacts) {
966                 my $xact_node = $xact->toXML({no_virt => 1})->documentElement;
967                 $user_doc->adoptNode($xact_node);
968                 $xacts_node->appendChild($xact_node);
969             }
970         }
971
972         print $FILE $user_doc->documentElement->toString(1) . "\n";
973     }
974
975     print $FILE "\n</Collections>";
976     close($FILE);
977
978     (my $complete_file = $file_name) =~ s|.tmp$|.xml|og;
979
980     unless (move($file_name, $complete_file)) {
981         $logger->error("collections: unable to move ".
982             "user_balance file $file_name => $complete_file : $@");
983     }
984
985     return undef;
986 }
987
988 sub setup_batch_file {
989     my $prefix = shift;
990     my $args = shift;
991     my $location = $$args{location};
992     my $start_date = $$args{start_date};
993     my $end_date = $$args{end_date};
994     my $user_id = $$args{user_id};
995
996     my $conf = OpenSRF::Utils::SettingsClient->new;
997     my $dir_name = $conf->config_value(apps =>
998         'open-ils.collections' => app_settings => 'batch_file_dir');
999
1000     if (!$dir_name) {
1001         $logger->error("collections: no batch_file_dir directory configured");
1002         return OpenILS::Event->new('COLLECTIONS_FILE_ERROR');
1003     }
1004
1005     unless (-e $dir_name) {
1006         eval { mkpath($dir_name); };
1007         if ($@) {
1008             $logger->error("collections: unable to create batch_file_dir directory $dir_name : $@");
1009             return OpenILS::Event->new('COLLECTIONS_FILE_ERROR');
1010         }
1011     }
1012
1013     my $file_prefix = "${prefix}_" . DateTime->now->strftime('%F') . "_$location";
1014     $file_prefix .= "_$start_date" if $start_date;
1015     $file_prefix .= "_$end_date" if $end_date;
1016     $file_prefix .= "_$user_id" if $user_id;
1017
1018     my $FILE;
1019     my $file_name = File::Spec->catfile($dir_name, "$file_prefix.tmp");
1020
1021     unless (open($FILE, '>', $file_name)) {
1022         $logger->error("collections: unable to open user_balance_summary file $file_name : $@");
1023         return OpenILS::Event->new('COLLECTIONS_FILE_ERROR');
1024     }
1025
1026     return (undef, $file_prefix, $file_name, $FILE);
1027 }
1028
1029 sub flesh_payment {
1030     my $e = shift;
1031     my $p = shift;
1032     my $type = $p->payment_type;
1033     $logger->debug("collect: fleshing workstation on payment $type : ".$p->id);
1034     my $meth = "retrieve_money_$type";
1035     $p = $e->$meth($p->id) or return (undef, $e->event);
1036     try {
1037         $p->payment_type($type);
1038         $p->cash_drawer(
1039             $e->retrieve_actor_workstation(
1040                 [
1041                     $p->cash_drawer,
1042                     {
1043                         flesh => 1,
1044                         flesh_fields => { aws => [ 'owning_lib' ] }
1045                     }
1046                 ]
1047             )
1048         );
1049     } catch Error with {};
1050     return ($p);
1051 }
1052
1053
1054 # --------------------------------------------------------------
1055 # Collect all open circs for the user
1056 # For each circ, see if any billings or payments were created
1057 # during the given time period.
1058 # --------------------------------------------------------------
1059 sub fetch_circ_xacts {
1060     my $e                = shift;
1061     my $uid            = shift;
1062     my $org            = shift;
1063     my $start_date = shift;
1064     my $end_date    = shift;
1065
1066     my @circs;
1067
1068     # at the specified org and each descendent org,
1069     # fetch the open circs for this user
1070     $U->walk_org_tree( $org,
1071         sub {
1072             my $n = shift;
1073             $logger->debug("collect: searching for open circs at " . $n->shortname);
1074             push( @circs,
1075                 @{
1076                     $e->search_action_circulation(
1077                         {
1078                             usr            => $uid,
1079                             circ_lib        => $n->id,
1080                         },
1081                         {idlist => 1}
1082                     )
1083                 }
1084             );
1085         }
1086     );
1087
1088
1089     my @data;
1090     my $active_ids = fetch_active($e, \@circs, $start_date, $end_date);
1091
1092     for my $cid (@$active_ids) {
1093         push( @data,
1094             $e->retrieve_action_circulation(
1095                 [
1096                     $cid,
1097                     {
1098                         flesh => 1,
1099                         flesh_fields => {
1100                             circ => [ "billings", "payments", "circ_lib", 'target_copy' ]
1101                         }
1102                     }
1103                 ]
1104             )
1105         );
1106     }
1107
1108     return \@data;
1109 }
1110
1111 sub fetch_grocery_xacts {
1112     my $e                = shift;
1113     my $uid            = shift;
1114     my $org            = shift;
1115     my $start_date = shift;
1116     my $end_date    = shift;
1117
1118     my @xacts;
1119     $U->walk_org_tree( $org,
1120         sub {
1121             my $n = shift;
1122             $logger->debug("collect: searching for open grocery xacts at " . $n->shortname);
1123             push( @xacts,
1124                 @{
1125                     $e->search_money_grocery(
1126                         {
1127                             usr                    => $uid,
1128                             billing_location    => $n->id,
1129                         },
1130                         {idlist => 1}
1131                     )
1132                 }
1133             );
1134         }
1135     );
1136
1137     my @data;
1138     my $active_ids = fetch_active($e, \@xacts, $start_date, $end_date);
1139
1140     for my $id (@$active_ids) {
1141         push( @data,
1142             $e->retrieve_money_grocery(
1143                 [
1144                     $id,
1145                     {
1146                         flesh => 1,
1147                         flesh_fields => {
1148                             mg => [ "billings", "payments", "billing_location" ] }
1149                     }
1150                 ]
1151             )
1152         );
1153     }
1154
1155     return \@data;
1156 }
1157
1158 sub fetch_reservation_xacts {
1159     my $e                = shift;
1160     my $uid            = shift;
1161     my $org            = shift;
1162     my $start_date = shift;
1163     my $end_date    = shift;
1164
1165     my @xacts;
1166     $U->walk_org_tree( $org,
1167         sub {
1168             my $n = shift;
1169             $logger->debug("collect: searching for open grocery xacts at " . $n->shortname);
1170             push( @xacts,
1171                 @{
1172                     $e->search_booking_reservation(
1173                         {
1174                             usr                    => $uid,
1175                             pickup_lib          => $n->id,
1176                         },
1177                         {idlist => 1}
1178                     )
1179                 }
1180             );
1181         }
1182     );
1183
1184     my @data;
1185     my $active_ids = fetch_active($e, \@xacts, $start_date, $end_date);
1186
1187     for my $id (@$active_ids) {
1188         push( @data,
1189             $e->retrieve_booking_reservation(
1190                 [
1191                     $id,
1192                     {
1193                         flesh => 1,
1194                         flesh_fields => {
1195                             bresv => [ "billings", "payments", "pickup_lib" ] }
1196                     }
1197                 ]
1198             )
1199         );
1200     }
1201
1202     return \@data;
1203 }
1204
1205
1206
1207 # --------------------------------------------------------------
1208 # Given a list of xact id's, this returns a list of id's that
1209 # had any activity within the given time span
1210 # --------------------------------------------------------------
1211 sub fetch_active {
1212     my( $e, $ids, $start_date, $end_date ) = @_;
1213
1214     # use this..
1215     # { payment_ts => { between => [ $start, $end ] } } ' ;)
1216
1217     my @active;
1218     for my $id (@$ids) {
1219
1220         # see if any billings were created in the given time range
1221         my $bills = $e->search_money_billing (
1222             {
1223                 xact            => $id,
1224                 billing_ts    => { between => [ $start_date, $end_date ] },
1225             },
1226             {idlist =>1}
1227         );
1228
1229         my $payments = [];
1230
1231         if( !@$bills ) {
1232
1233             # see if any payments were created in the given range
1234             $payments = $e->search_money_payment (
1235                 {
1236                     xact            => $id,
1237                     payment_ts    => { between => [ $start_date, $end_date ] },
1238                 },
1239                 {idlist =>1}
1240             );
1241         }
1242
1243
1244         push( @active, $id ) if @$bills or @$payments;
1245     }
1246
1247     return \@active;
1248 }
1249
1250
1251 __PACKAGE__->register_method(
1252     method    => 'create_user_note',
1253     api_name  => 'open-ils.collections.patron_note.create',
1254     api_level => 1,
1255     argc      => 4,
1256     signature => {
1257         desc     => q/ Adds a note to a patron's account /,
1258         params   => [
1259             {    name => 'auth',
1260                 desc => 'The authentication token',
1261                 type => 'string' },
1262
1263             {    name => 'user_barcode',
1264                 desc => q/The patron's barcode/,
1265                 type => q/string/,
1266             },
1267             {    name => 'title',
1268                 desc => q/The title of the note/,
1269                 type => q/string/,
1270             },
1271
1272             {    name => 'note',
1273                 desc => q/The text of the note/,
1274                 type => q/string/,
1275             },
1276         ],
1277
1278           'return' => {
1279             desc        => q/
1280                 Returns SUCCESS event on success, error event otherwise.
1281                 /,
1282             type        => 'object'
1283         }
1284     }
1285 );
1286
1287
1288 sub create_user_note {
1289     my( $self, $conn, $auth, $user_barcode, $title, $note_txt ) = @_;
1290
1291     my $e = new_editor(authtoken=>$auth, xact=>1);
1292     return $e->event unless $e->checkauth;
1293     return $e->event unless $e->allowed('UPDATE_USER'); # XXX Makre more specific perm for this
1294
1295     return $e->event unless
1296         my $card = $e->search_actor_card({barcode=>$user_barcode})->[0];
1297
1298     my $note = Fieldmapper::actor::usr_note->new;
1299     $note->usr($card->usr);
1300     $note->title($title);
1301     $note->creator($e->requestor->id);
1302     $note->create_date('now');
1303     $note->pub('f');
1304     $note->value($note_txt);
1305
1306     $e->create_actor_usr_note($note) or return $e->event;
1307     $e->commit;
1308     return OpenILS::Event->new('SUCCESS');
1309 }
1310
1311
1312
1313 1;