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