From 375054cfe65304c5717f13c7fbe15e66183ff2ab Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Thu, 12 Apr 2012 11:44:44 -0400 Subject: [PATCH] Collections user balance API / batch file output New API open-ils.collections.user_balance_summary.generate, which generates summary information on patron balance owed for all open transactions that occurred at the requested location. Optional flag supports including per-xact summary information as well. The summary information is written to an XML file and placed in a protected, shared web directory for download after the file has been generated. During creation, the output file will have a .tmp suffix, allowing clients to poll for file completion. Includes a new opensrf.xml app_setting for open-ils.collections to specifiy the batch file directory as well as a sample Apache config in eg_vhost.conf Signed-off-by: Bill Erickson Signed-off-by: Lebbeous Fogle-Weekley --- Open-ILS/examples/apache/eg_vhost.conf | 14 ++ Open-ILS/examples/opensrf.xml.example | 4 + .../lib/OpenILS/Application/Collections.pm | 229 ++++++++++++++++++ 3 files changed, 247 insertions(+) diff --git a/Open-ILS/examples/apache/eg_vhost.conf b/Open-ILS/examples/apache/eg_vhost.conf index c0ff745936..0265d6666a 100644 --- a/Open-ILS/examples/apache/eg_vhost.conf +++ b/Open-ILS/examples/apache/eg_vhost.conf @@ -432,6 +432,20 @@ RewriteRule . - [E=locale:en-US] allow from all + + SetHandler perl-script + AuthType Basic + AuthName "Collections Login" + PerlOptions +GlobalRequest + PerlSetVar OILSProxyPermissions "money.collections_tracker.create" + PerlAuthenHandler OpenILS::WWW::Proxy::Authen + require valid-user + Options +ExecCGI + PerlSendHeader On + allow from all + + + # ---------------------------------------------------------------------------------- # Reporting output lives here # ---------------------------------------------------------------------------------- diff --git a/Open-ILS/examples/opensrf.xml.example b/Open-ILS/examples/opensrf.xml.example index b444cf32fc..be398d63ed 100644 --- a/Open-ILS/examples/opensrf.xml.example +++ b/Open-ILS/examples/opensrf.xml.example @@ -1062,6 +1062,10 @@ vim:et:ts=4:sw=4: 1 5 + + + /openils/var/web/collections + diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Collections.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Collections.pm index 97c07f7361..3979d208ce 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Collections.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Collections.pm @@ -11,6 +11,11 @@ use OpenILS::Utils::CStoreEditor qw/:funcs/; use OpenILS::Event; use OpenILS::Const qw/:const/; my $U = "OpenILS::Application::AppUtils"; +use XML::LibXML; +use Scalar::Util 'blessed'; +use File::Spec; +use File::Copy; +use File::Path; # -------------------------------------------------------------- @@ -788,6 +793,230 @@ sub transaction_details { return \@data; } +__PACKAGE__->register_method( + method => 'user_balance_summary', + api_name => 'open-ils.collections.user_balance_summary.generate', + api_level => 1, + stream => 1, + argc => 2, + signature => { + desc => q/Collect balance information for users in collections. By default, + only the total balance owed is calculated. Use the "include_xacts" + param to include per-transaction summaries as well./, + params => [ + { name => 'auth', + desc => 'The authentication token', + type => 'string' }, + { name => 'args', + desc => q/ + Hash of API arguments. Options include: + location -- org unit shortname + start_date -- ISO 8601 date. limit to patrons added to collections on or after this date (optional). + end_date -- ISO 8601 date. limit to patrons added to collections on or before this date (optional). + user_id -- retrieve information only for this user (takes preference over + start and end_date). May be a single ID or list of IDs. (optional). + include_xacts -- If true, include a summary object per transaction in addition to the full balance owed + /, + type => q/hash/ + }, + ], + 'return' => { + desc => q/ + The file name prefix of the file to be created. + The file name format will be: + user_balance_YYYY-MM-DD_${location}_${start_date}_${end_date}_${user_id}.[tmp|xml] + Optional params not provided by the caller will not be part of the file name. + Examples: + user_balance_BR1_2012-05-25_2012-01-01_2012-12-31 # start and end dates + user_balance_BR2_2012-05-25_153244 # user id only. + In-process files will have a .tmp suffix + Completed files will have a .xml suffix + /, + type => 'string' + } + } +); + +sub user_balance_summary { + my ($self, $client, $auth, $args) = @_; + + my $location = $$args{location}; + my $start_date = $$args{start_date}; + my $end_date = $$args{end_date}; + my $user_id = $$args{user_id}; + + return OpenILS::Event->new('BAD_PARAMS') + unless $auth and $location and + ($start_date or $end_date or $user_id); + + my $e = new_editor(authtoken => $auth); + return $e->event unless $e->checkauth; + + my $org = $e->search_actor_org_unit({shortname => $location})->[0] + or return $e->event; + + # they need global perms to view users so no org is provided + return $e->event unless $e->allowed('VIEW_USER', $org->id); + + my $org_list = $U->get_org_descendants($org->id); + + my ($evt, $file_prefix, $file_name, $FILE) = setup_batch_file('user_balance', $args); + + $client->respond_complete($evt || $file_prefix); + + my @user_list; + + if ($user_id) { + @user_list = (ref $user_id eq 'ARRAY') ? @$user_id : ($user_id); + + } else { + # collect the users from the tracker table based on the provided filters + + my $query = { + select => {mct => ['usr']}, + from => 'mct', + where => {location => $org_list} + }; + + $query->{where}->{enter_time} = {'>=' => $start_date}; + $query->{where}->{enter_time} = {'<=' => $end_date}; + my $users = $e->json_query($query); + @user_list = map {$_->{usr}} @$users; + } + + print $FILE "\n"; # append to the document as we have data + + for my $user_id (@user_list) { + my $user_doc = XML::LibXML::Document->new; + my $root = $user_doc->createElement('User'); + $user_doc->setDocumentElement($root); + + my $user = $e->retrieve_actor_user([ + $user_id, { + flesh => 1, + flesh_fields => { + au => [ + 'card', + 'cards', + 'standing_penalties', + 'addresses', + 'billing_address', + 'mailing_address', + 'stat_cat_entries' + ] + }} + ]); + + my $au_doc = $user->toXML({no_virt => 1, skip_fields => {au => ['passwd']}}); + my $au_node = $au_doc->documentElement; + $user_doc->adoptNode($au_node); + $root->appendChild($au_node); + + my $circ_ids = $e->search_action_circulation( + {usr => $user_id, circ_lib => $org_list, xact_finish => undef}, + {idlist => 1} + ); + + my $groc_ids = $e->search_money_grocery( + {usr => $user_id, billing_location => $org_list, xact_finish => undef}, + {idlist => 1} + ); + + my $res_ids = $e->search_booking_reservation( + {usr => $user_id, pickup_lib => $org_list, xact_finish => undef}, + {idlist => 1} + ); + + # get the sum owed an all transactions + my $balance = $e->json_query({ + select => {mbts => [ + { column => 'balance_owed', + transform => 'sum', + aggregate => 1 + } + ]}, + from => 'mbts', + where => {id => [@$circ_ids, @$groc_ids, @$res_ids]} + })->[0]; + + $balance = $balance ? $balance->{balance_owed} : '0'; + + my $xacts_node = $user_doc->createElement('Transactions'); + my $balance_node = $user_doc->createElement('BalanceOwed'); + $balance_node->appendChild($user_doc->createTextNode($balance)); + $xacts_node->appendChild($balance_node); + $root->appendChild($xacts_node); + + if ($$args{include_xacts}) { + my $xacts = $e->search_money_billable_transaction_summary( + {id => [@$circ_ids, @$groc_ids, @$res_ids]}, + {substream => 1} + ); + + for my $xact (@$xacts) { + my $xact_node = $xact->toXML({no_virt => 1})->documentElement; + $user_doc->adoptNode($xact_node); + $xacts_node->appendChild($xact_node); + } + } + + print $FILE $user_doc->documentElement->toString(1) . "\n"; + } + + print $FILE "\n"; + close($FILE); + + (my $complete_file = $file_name) =~ s|.tmp$|.xml|og; + + unless (move($file_name, $complete_file)) { + $logger->error("collections: unable to move ". + "user_balance file $file_name => $complete_file : $@"); + } + + return undef; +} + +sub setup_batch_file { + my $prefix = shift; + my $args = shift; + my $location = $$args{location}; + my $start_date = $$args{start_date}; + my $end_date = $$args{end_date}; + my $user_id = $$args{user_id}; + + my $conf = OpenSRF::Utils::SettingsClient->new; + my $dir_name = $conf->config_value(apps => + 'open-ils.collections' => app_settings => 'batch_file_dir'); + + if (!$dir_name) { + $logger->error("collections: no batch_file_dir directory configured"); + return OpenILS::Event->new('COLLECTIONS_FILE_ERROR'); + } + + unless (-e $dir_name) { + eval { mkpath($dir_name); }; + if ($@) { + $logger->error("collections: unable to create batch_file_dir directory $dir_name : $@"); + return OpenILS::Event->new('COLLECTIONS_FILE_ERROR'); + } + } + + my $file_prefix = "${prefix}_" . DateTime->now->strftime('%F') . "_$location"; + $file_prefix .= "_$start_date" if $start_date; + $file_prefix .= "_$end_date" if $end_date; + $file_prefix .= "_$user_id" if $user_id; + + my $FILE; + my $file_name = File::Spec->catfile($dir_name, "$file_prefix.tmp"); + + unless (open($FILE, '>', $file_name)) { + $logger->error("collections: unable to open user_balance_summary file $file_name : $@"); + return OpenILS::Event->new('COLLECTIONS_FILE_ERROR'); + } + + return (undef, $file_prefix, $file_name, $FILE); +} + sub flesh_payment { my $e = shift; my $p = shift; -- 2.43.2