From b649140060793a2bed8ff9dbedc7c25851c2fe06 Mon Sep 17 00:00:00 2001 From: Dan Wells Date: Mon, 12 Feb 2018 14:47:21 -0500 Subject: [PATCH] LP#1748986 Billing Statement View Over time, Evergreen has developed a rich set of features to support various billing scenarios. Unfortunately, our interface has not kept up in some ways, and this leads to some confusion at times, particularly for front line staff. One way to bring clarity is to apply better grouping, labeling, and ordering of the various transactional events. By doing so, we can generate a billing "statement" with similarities to the statements we regularly encounter when dealing with other financial institutions. This branch does so for the staff client view, but it also seems viable to carry over the same idea to an eventual patron (or print) view as well. Signed-off-by: Dan Wells Signed-off-by: Kathy Lussier --- .../lib/OpenILS/Application/Circ/Money.pm | 136 ++++++++++++++++++ .../staff/circ/patron/t_xact_details.tt2 | 71 ++------- .../circ/patron/t_xact_details_details.tt2 | 59 ++++++++ .../circ/patron/t_xact_details_statement.tt2 | 72 ++++++++++ .../js/ui/default/staff/circ/patron/app.js | 2 +- .../js/ui/default/staff/circ/patron/bills.js | 25 +++- 6 files changed, 304 insertions(+), 61 deletions(-) create mode 100644 Open-ILS/src/templates/staff/circ/patron/t_xact_details_details.tt2 create mode 100644 Open-ILS/src/templates/staff/circ/patron/t_xact_details_statement.tt2 diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm index 17c496bdd9..f177e5d120 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm @@ -32,6 +32,9 @@ use OpenILS::Utils::Penalty; use Business::Stripe; $Data::Dumper::Indent = 0; use OpenILS::Const qw/:const/; +use OpenSRF::Utils qw/:datetime/; +use DateTime::Format::ISO8601; +my $parser = DateTime::Format::ISO8601->new; sub get_processor_settings { my $e = shift; @@ -1273,4 +1276,137 @@ sub retrieve_credit_payable_balance { } +__PACKAGE__->register_method( + method => "retrieve_statement", + authoritative => 1, + api_name => "open-ils.circ.money.statement.retrieve", + notes => "Returns an organized summary of a billable transaction, including all bills, payments, adjustments, and voids." + ); + +sub _to_epoch { + my $ts = shift @_; + + return $parser->parse_datetime(cleanse_ISO8601($ts))->epoch; +} + +my %_statement_sort = ( + 'billing' => 0, + 'account_adjustment' => 1, + 'void' => 2, + 'payment' => 3 +); + +sub retrieve_statement { + my ( $self, $client, $auth, $xact_id ) = @_; + + my $e = new_editor(authtoken=>$auth); + return $e->event unless $e->checkauth; + return $e->event unless $e->allowed('VIEW_TRANSACTION'); + + # XXX: move this lookup login into a DB query? + my @line_prep; + + # collect all payments/adjustments + my $payments = $e->search_money_payment({ xact => $xact_id }); + foreach my $payment (@$payments) { + my $type = $payment->payment_type; + $type = 'payment' if $type ne 'account_adjustment'; + push(@line_prep, [$type, _to_epoch($payment->payment_ts), $payment->payment_ts, $payment->id, $payment]); + } + + # collect all billings + my $billings = $e->search_money_billing({ xact => $xact_id }); + foreach my $billing (@$billings) { + if ($U->is_true($billing->voided)){ + push(@line_prep, ['void', _to_epoch($billing->void_time), $billing->void_time, $billing->id, $billing]); # voids get two entries, one to represent the bill event, one for the void event + } + push(@line_prep, ['billing', _to_epoch($billing->billing_ts), $billing->billing_ts, $billing->id, $billing]); + } + + # order every event by timestamp, then bills/adjustments/voids/payments order, then id + my @ordered_line_prep = sort { + $a->[1] <=> $b->[1] + || + $_statement_sort{$a->[0]} <=> $_statement_sort{$b->[0]} + || + $a->[3] <=> $b->[3] + } @line_prep; + + # let's start building the statement structure + my (@lines, %current_line, $running_balance); + foreach my $event (@ordered_line_prep) { + my $obj = $event->[4]; + my $type = $event->[0]; + my $ts = $event->[2]; + my $billing_type = $type =~ /billing|void/ ? $obj->billing_type : ''; # TODO: get non-legacy billing type + my $note = $obj->note || ''; + # last line should be void information, try to isolate it + if ($type eq 'billing' and $obj->voided) { + $note =~ s/\n.*$//; + } elsif ($type eq 'void') { + $note = (split(/\n/, $note))[-1]; + } + + # if we have new details, start a new line + if ($current_line{amount} and ( + $type ne $current_line{type} + or ($note ne $current_line{note}) + or ($billing_type ne $current_line{billing_type}) + ) + ) { + push(@lines, {%current_line}); # push a copy of the hash, not the real thing + %current_line = (); + } + if (!$current_line{type}) { + $current_line{type} = $type; + $current_line{billing_type} = $billing_type; + $current_line{note} = $note; + } + if (!$current_line{start_date}) { + $current_line{start_date} = $ts; + } elsif ($ts ne $current_line{start_date}) { + $current_line{end_date} = $ts; + } + $current_line{amount} += $obj->amount; + if ($current_line{details}) { + push(@{$current_line{details}}, $obj); + } else { + $current_line{details} = [$obj]; + } + } + push(@lines, {%current_line}); # push last one on + + # get/update totals, format notes + my %totals = ( + billing => 0, + payment => 0, + account_adjustment => 0, + void => 0 + ); + foreach my $line (@lines) { + $totals{$line->{type}} += $line->{amount}; + if ($line->{type} eq 'billing') { + $running_balance += $line->{amount}; + } else { # not a billing; balance goes down for everything else + $running_balance -= $line->{amount}; + } + $line->{running_balance} = $running_balance; + $line->{note} = $line->{note} ? [split(/\n/, $line->{note})] : []; + } + + return { + xact_id => $xact_id, + summary => { + balance_due => $totals{billing} - ($totals{payment} + $totals{account_adjustment} + $totals{void}), + billing_total => $totals{billing}, + credit_total => $totals{payment} + $totals{account_adjustment}, + payment_total => $totals{payment}, + account_adjustment_total => $totals{account_adjustment}, + void_total => $totals{void} + }, + lines => \@lines + } +} + + 1; diff --git a/Open-ILS/src/templates/staff/circ/patron/t_xact_details.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_xact_details.tt2 index fdab723cd8..c7c5d1bac7 100644 --- a/Open-ILS/src/templates/staff/circ/patron/t_xact_details.tt2 +++ b/Open-ILS/src/templates/staff/circ/patron/t_xact_details.tt2 @@ -105,61 +105,18 @@ - -
- - - - - - - - - - -
- - - - - - {{item.staff_name}} ({{item.staff_barcode}}) @ {{item.staff_org}} - - - - - - - - - - - - - - - - - + +[% INCLUDE 'staff/circ/patron/t_xact_details_statement.tt2' %] +[% INCLUDE 'staff/circ/patron/t_xact_details_details.tt2' %] diff --git a/Open-ILS/src/templates/staff/circ/patron/t_xact_details_details.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_xact_details_details.tt2 new file mode 100644 index 0000000000..6cd026a801 --- /dev/null +++ b/Open-ILS/src/templates/staff/circ/patron/t_xact_details_details.tt2 @@ -0,0 +1,59 @@ +
+ + + + + + + + + + + + +
+ + + + + + {{item.staff_name}} ({{item.staff_barcode}}) @ {{item.staff_org}} + + + + + + + + + + + + + + + + + +
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_xact_details_statement.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_xact_details_statement.tt2 new file mode 100644 index 0000000000..7dc5cead6c --- /dev/null +++ b/Open-ILS/src/templates/staff/circ/patron/t_xact_details_statement.tt2 @@ -0,0 +1,72 @@ +
+ +

[% l('Billing Statement') %]

+
+
+
+
[% l('Type') %]
+
[% l('Description') %]
+
[% l('Amount') %]
+
[% l('Balance') %]
+
+
+
+
+
+ [% l('Billing') %] + [% l('Payment') %] + [% l('Adjustment') %] + [% l('Void') %] +
+
+ {{line.billing_type}}
+ {{line.note | join:', '}}
+ {{line.start_date | date:$root.egDateAndTimeFormat}} - {{line.end_date | date:$root.egDateAndTimeFormat}} +
+
+ -{{line.amount | currency}} +
+
+ {{line.running_balance | currency}} +
+
+
+
+
+
+
[% l('Total Charges') %]
+
+ {{statement_data.summary.billing_total | currency}} +
+
+
+
[% l('Total Payments') %]
+
+ -{{statement_data.summary.payment_total || 0 | currency}} +
+
+
+
[% l('Total Adjustments') %]
+
+ -{{statement_data.summary.account_adjustment_total | currency}} +
+
+
+
[% l('Total Voids') %]
+
+ -{{statement_data.summary.void_total | currency}} +
+
+
+
+
[% l('Balance Due') %]
+
+ {{statement_data.summary.balance_due | currency}} +
+
+
+
+
+
+ +
diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/app.js b/Open-ILS/web/js/ui/default/staff/circ/patron/app.js index a52c8b8a05..580b430e0b 100644 --- a/Open-ILS/web/js/ui/default/staff/circ/patron/app.js +++ b/Open-ILS/web/js/ui/default/staff/circ/patron/app.js @@ -138,7 +138,7 @@ angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 'egUserBucketMod', resolve : resolver }); - $routeProvider.when('/circ/patron/:id/bill/:xact_id', { + $routeProvider.when('/circ/patron/:id/bill/:xact_id/:xact_tab', { templateUrl: './circ/patron/t_xact_details', controller: 'XactDetailsCtrl', resolve : resolver diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/bills.js b/Open-ILS/web/js/ui/default/staff/circ/patron/bills.js index d9e1bf79c9..392b696179 100644 --- a/Open-ILS/web/js/ui/default/staff/circ/patron/bills.js +++ b/Open-ILS/web/js/ui/default/staff/circ/patron/bills.js @@ -87,6 +87,17 @@ function($q , egCore , egWorkLog , patronSvc) { ); } + service.fetchStatement = function(xact_id) { + return egCore.net.request( + 'open-ils.circ', + 'open-ils.circ.money.statement.retrieve', + egCore.auth.token(), xact_id + ).then(function(resp) { + if (evt = egCore.evt.parse(resp)) return alert(evt); + return resp; + }); + } + // TODO: no longer needed? service.fetchPayments = function(xact_id) { return egCore.net.request( @@ -723,7 +734,7 @@ function($scope , $q , $routeParams , egCore , egConfirmDialog , $location, $scope.showFullDetails = function(all) { if (all[0]) $location.path('/circ/patron/' + - patronSvc.current.id() + '/bill/' + all[0].id); + patronSvc.current.id() + '/bill/' + all[0].id + '/statement'); } $scope.activateBill = function(xact) { @@ -741,6 +752,7 @@ function($scope, $q , $routeParams , egCore , egGridDataProvider , patronSvc , $scope.initTab('bills', $routeParams.id); var xact_id = $routeParams.xact_id; + $scope.xact_tab = $routeParams.xact_tab; var xactGrid = $scope.xactGridControls = { setQuery : function() { return {xact : xact_id} }, @@ -827,6 +839,13 @@ function($scope, $q , $routeParams , egCore , egGridDataProvider , patronSvc , } // -- retrieve our data + if ($scope.xact_tab == 'statement') { + //fetch combined billing statement data + billSvc.fetchStatement(xact_id).then(function(statement) { + //console.log(statement); + $scope.statement_data = statement; + }); + } $scope.total_circs = 0; // start with 0 instead of undefined egBilling.fetchXact(xact_id).then(function(xact) { $scope.xact = xact; @@ -940,7 +959,7 @@ function($scope, $q , egCore , patronSvc , billSvc , egPromptDialog , $location $scope.showFullDetails = function(all) { if (all[0]) $location.path('/circ/patron/' + - patronSvc.current.id() + '/bill/' + all[0].id); + patronSvc.current.id() + '/bill/' + all[0].id + '/statement'); } // For now, only adds billing to first selected item. @@ -1043,7 +1062,7 @@ function($scope, $q , egCore , patronSvc , billSvc , $location) { $scope.showFullDetails = function(all) { if (all[0]) $location.path('/circ/patron/' + - patronSvc.current.id() + '/bill/' + all[0]['xact.id']); + patronSvc.current.id() + '/bill/' + all[0]['xact.id'] + '/statement'); } $scope.totals.selected_paid = function() { -- 2.43.2