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;
}
+__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;
</div>
</div>
-
-<!-- set a lower default page size (limit) to allow for more space -->
<hr/>
-<eg-grid
- main-label="[% l('Bills') %]"
- idl-class="mb"
- id-field="id"
- grid-controls="xactGridControls"
- auto-fields="true"
- page-size="10"
- dateformat="{{$root.egDateAndTimeFormat}}">
-
- <eg-grid-action
- label="[% l('Void Billings') %]" handler="voidBillings"></eg-grid-action>
-
- <eg-grid-action
- label="[% l('Edit Note') %]" handler="editBillNotes"></eg-grid-action>
-
-</eg-grid>
-
-<!-- TODO: this grid may contain objects (payments) of different types..
- apply manual columns, see xul -->
-<!-- NOTE: sorting disabled since payments are fetched via non-sortable API -->
-<br/>
-<eg-grid
- main-label="[% l('Payments') %]"
- idl-class="mbp"
- id-field="id"
- grid-controls="paymentGridControls"
- page-size="10"
- dateformat="{{$root.egDateAndTimeFormat}}">
- <eg-grid-action
- label="[% l('Edit Note') %]" handler="editPaymentNotes"></eg-grid-action>
-
- <eg-grid-field path="cash_payment.cash_drawer.name" parent-idl-class="mbp"
- label="[% l('Cash Drawer') %]"></eg-grid-field>
-
- <eg-grid-field path="accepting_usr">{{item.staff_name}} ({{item.staff_barcode}}) @ {{item.staff_org}}</eg-grid-field>
-
- <eg-grid-field path="amount"></eg-grid-field>
- <eg-grid-field path="id"></eg-grid-field>
- <eg-grid-field path="note"></eg-grid-field>
- <eg-grid-field path="payment_ts"></eg-grid-field>
- <eg-grid-field path="payment_type"></eg-grid-field>
- <eg-grid-field path="xact"></eg-grid-field>
- <eg-grid-field path="voided"></eg-grid-field>
-
- <eg-grid-field path="accepting_usr.family_name" name="staff_name"
- label="[% l('Staff Last Name') %]" hidden required></eg-grid-field>
-
- <eg-grid-field path="accepting_usr.card.barcode" name="staff_barcode"
- label="[% l('Staff Barcode') %]" hidden required></eg-grid-field>
-
- <eg-grid-field path="accepting_usr.home_ou.shortname" name="staff_org"
- label="[% l('Staff Org Unit') %]" hidden required></eg-grid-field>
-
-</eg-grid>
-
+<ul class="nav nav-tabs">
+ <li ng-class="{active : xact_tab == 'statement'}">
+ <a href="./circ/patron/{{patron().id()}}/bill/{{xact.id()}}/statement">
+ [% l('Statement') %]
+ </a>
+ </li>
+ <li ng-class="{active : xact_tab == 'details'}">
+ <a href="./circ/patron/{{patron().id()}}/bill/{{xact.id()}}/details">
+ [% l('Details') %]
+ </a>
+ </li>
+</ul>
+[% INCLUDE 'staff/circ/patron/t_xact_details_statement.tt2' %]
+[% INCLUDE 'staff/circ/patron/t_xact_details_details.tt2' %]
--- /dev/null
+<div ng-if="xact_tab == 'details'">
+
+<!-- set a lower default page size (limit) to allow for more space -->
+<eg-grid
+ main-label="[% l('Bills') %]"
+ idl-class="mb"
+ id-field="id"
+ grid-controls="xactGridControls"
+ auto-fields="true"
+ page-size="10"
+ dateformat="{{$root.egDateAndTimeFormat}}">
+
+ <eg-grid-action
+ label="[% l('Void Billings') %]" handler="voidBillings"></eg-grid-action>
+
+ <eg-grid-action
+ label="[% l('Edit Note') %]" handler="editBillNotes"></eg-grid-action>
+
+</eg-grid>
+
+<!-- TODO: this grid may contain objects (payments) of different types..
+ apply manual columns, see xul -->
+<!-- NOTE: sorting disabled since payments are fetched via non-sortable API -->
+<br/>
+<eg-grid
+ main-label="[% l('Payments') %]"
+ idl-class="mbp"
+ id-field="id"
+ grid-controls="paymentGridControls"
+ page-size="10"
+ dateformat="{{$root.egDateAndTimeFormat}}">
+ <eg-grid-action
+ label="[% l('Edit Note') %]" handler="editPaymentNotes"></eg-grid-action>
+
+ <eg-grid-field path="cash_payment.cash_drawer.name" parent-idl-class="mbp"
+ label="[% l('Cash Drawer') %]"></eg-grid-field>
+
+ <eg-grid-field path="accepting_usr">{{item.staff_name}} ({{item.staff_barcode}}) @ {{item.staff_org}}</eg-grid-field>
+
+ <eg-grid-field path="amount"></eg-grid-field>
+ <eg-grid-field path="id"></eg-grid-field>
+ <eg-grid-field path="note"></eg-grid-field>
+ <eg-grid-field path="payment_ts"></eg-grid-field>
+ <eg-grid-field path="payment_type"></eg-grid-field>
+ <eg-grid-field path="xact"></eg-grid-field>
+ <eg-grid-field path="voided"></eg-grid-field>
+
+ <eg-grid-field path="accepting_usr.family_name" name="staff_name"
+ label="[% l('Staff Last Name') %]" hidden required></eg-grid-field>
+
+ <eg-grid-field path="accepting_usr.card.barcode" name="staff_barcode"
+ label="[% l('Staff Barcode') %]" hidden required></eg-grid-field>
+
+ <eg-grid-field path="accepting_usr.home_ou.shortname" name="staff_org"
+ label="[% l('Staff Org Unit') %]" hidden required></eg-grid-field>
+
+</eg-grid>
+
+</div>
--- /dev/null
+<div ng-if="xact_tab == 'statement'">
+
+<h4>[% l('Billing Statement') %]</h4>
+<div class="panel panel-default">
+ <div class="panel-heading">
+ <div class="row">
+ <div class="col-md-2">[% l('Type') %]</div>
+ <div class="col-md-6">[% l('Description') %]</div>
+ <div class="col-md-2">[% l('Amount') %]</div>
+ <div class="col-md-2">[% l('Balance') %]</div>
+ </div>
+ </div>
+ <div class="panel-body flex-container-striped">
+ <div class="row" ng-repeat="line in statement_data.lines">
+ <div class="col-md-2" ng-switch="line.type">
+ <span ng-switch-when="billing">[% l('Billing') %]</span>
+ <span ng-switch-when="payment">[% l('Payment') %]</span>
+ <span ng-switch-when="account_adjustment">[% l('Adjustment') %]</span>
+ <span ng-switch-when="void">[% l('Void') %]</span>
+ </div>
+ <div class="col-md-6">
+ <span ng-if="line.billing_type" class="strong-text">{{line.billing_type}}<br/></span>
+ <span ng-if="line.note.length" ng-class="{'strong-text' : !line.billing_type}">{{line.note | join:', '}}<br/></span>
+ <span class="small">{{line.start_date | date:$root.egDateAndTimeFormat}}</span><span ng-if="line.end_date" class="small"> - {{line.end_date | date:$root.egDateAndTimeFormat}}</span>
+ </div>
+ <div ng-style="line.type != 'billing' && {'color':'red'}" class="col-md-2">
+ <span ng-if="line.type != 'billing'">-</span>{{line.amount | currency}}
+ </div>
+ <div class="col-md-2">
+ {{line.running_balance | currency}}
+ </div>
+ </div>
+ <hr/>
+ <div class="row">
+ <div class="col-md-4 col-md-offset-8">
+ <div class="row">
+ <div class="col-md-6 strong-text">[% l('Total Charges') %]</div>
+ <div class="col-md-6 text-right">
+ {{statement_data.summary.billing_total | currency}}
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-6 strong-text">[% l('Total Payments') %]</div>
+ <div class="col-md-6 text-right" style="color:red">
+ -{{statement_data.summary.payment_total || 0 | currency}}
+ </div>
+ </div>
+ <div ng-if="statement_data.summary.account_adjustment_total > 0" class="row">
+ <div class="col-md-6 strong-text">[% l('Total Adjustments') %]</div>
+ <div class="col-md-6 text-right" style="color:red">
+ -{{statement_data.summary.account_adjustment_total | currency}}
+ </div>
+ </div>
+ <div ng-if="statement_data.summary.void_total > 0" class="row">
+ <div class="col-md-6 strong-text">[% l('Total Voids') %]</div>
+ <div class="col-md-6 text-right" style="color:red">
+ -{{statement_data.summary.void_total | currency}}
+ </div>
+ </div>
+ <hr/>
+ <div class="row">
+ <div class="col-md-6 strong-text">[% l('Balance Due') %]</div>
+ <div class="col-md-6 text-right">
+ {{statement_data.summary.balance_due | currency}}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+</div>
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
);
}
+ 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(
$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) {
$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} },
}
// -- 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;
$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.
$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() {