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