LP#1759343 Fix annotate payment setting name
[evergreen-equinox.git] / Open-ILS / web / js / ui / default / staff / circ / patron / bills.js
index 35bab20..308fb30 100644 (file)
@@ -12,9 +12,12 @@ function($q , egCore , egWorkLog , patronSvc) {
     // fetch org unit settings specific to the bills display
     service.fetchBillSettings = function() {
         if (service.settings) return $q.when(service.settings);
-        return egCore.org.settings(
-            ['ui.circ.billing.uncheck_bills_and_unfocus_payment_box','ui.circ.billing.amount_warn','ui.circ.billing.amount_limit']
-        ).then(function(s) {return service.settings = s});
+        return egCore.org.settings([
+            'ui.circ.billing.uncheck_bills_and_unfocus_payment_box',
+            'ui.circ.billing.amount_warn', 'ui.circ.billing.amount_limit',
+            'circ.staff_client.do_not_auto_attempt_print',
+            'circ.disable_patron_credit'
+        ]).then(function(s) {return service.settings = s});
     }
 
     // user billing summary
@@ -24,7 +27,9 @@ function($q , egCore , egWorkLog , patronSvc) {
         .then(function(summary) {return service.summary = summary})
     }
 
-    service.applyPayment = function(type, payments, note, check) {
+    service.applyPayment = function(
+        type, payments, note, check, cc_args, patron_credit) {
+
         return egCore.net.request(
             'open-ils.circ',
             'open-ils.circ.money.payment',
@@ -34,11 +39,21 @@ function($q , egCore , egWorkLog , patronSvc) {
                 payment_type : type,
                 check_number : check,
                 payments : payments,
-                patron_credit : 0
+                patron_credit : patron_credit,
+                cc_args : cc_args
             },
             patronSvc.current.last_xact_id()
         ).then(function(resp) {
             console.debug('payments: ' + js2JSON(resp));
+
+            if (evt = egCore.evt.parse(resp)) {
+                // Ideally, all scenarios that lead to this alert appearing
+                // will be avoided by the application logic.  Leave the alert
+                // in place now to root out any that remain to be addressed.
+                alert(evt);
+                return $q.reject(''+evt);
+            }
+
             var total = 0; angular.forEach(payments,function(p) { total += p[1]; });
             var msg;
             switch(type) {
@@ -57,12 +72,14 @@ function($q , egCore , egWorkLog , patronSvc) {
                     'total_amount' : total
                 }
             );
-            if (evt = egCore.evt.parse(resp)) 
-                return alert(evt);
 
             // payment API returns the update xact id so we can track it
             // for future payments without having to refresh the user.
             patronSvc.current.last_xact_id(resp.last_xact_id);
+
+            // reload patron data if credit balance has changed:
+            if(type === 'credit_payment' || patron_credit){ patronSvc.refreshPrimary(); }
+
             return resp.payments;
         });
     }
@@ -79,6 +96,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(
@@ -99,6 +127,18 @@ function($q , egCore , egWorkLog , patronSvc) {
         });
     }
 
+    service.adjustBillsToZero = function(bill_ids) {
+        return egCore.net.request(
+            'open-ils.circ',
+            'open-ils.circ.money.billable_xact.adjust_to_zero',
+            egCore.auth.token(),
+            bill_ids
+        ).then(function(resp) {
+            if (evt = egCore.evt.parse(resp)) return alert(evt);
+            return resp;
+        });
+    }
+
     service.updateBillNotes = function(note, ids) {
         return egCore.net.requestWithParamList(
             'open-ils.circ',
@@ -131,10 +171,10 @@ function($q , egCore , egWorkLog , patronSvc) {
 .controller('PatronBillsCtrl',
        ['$scope','$q','$routeParams','egCore','egConfirmDialog','$location',
         'egGridDataProvider','billSvc','patronSvc','egPromptDialog', 'egAlertDialog',
-        'egBilling',
+        'egBilling','$uibModal',
 function($scope , $q , $routeParams , egCore , egConfirmDialog , $location,
          egGridDataProvider , billSvc , patronSvc , egPromptDialog, egAlertDialog,
-         egBilling) {
+         egBilling , $uibModal) {
 
     $scope.initTab('bills', $routeParams.id);
     billSvc.userId = $routeParams.id;
@@ -147,10 +187,23 @@ function($scope , $q , $routeParams , egCore , egConfirmDialog , $location,
     $scope.focus_payment = true;
     $scope.annotate_payment = false;
     $scope.receipt_count = 1;
-    $scope.receipt_on_pay = false;
+    $scope.receipt_on_pay = { isChecked: false };
+    $scope.convert_to_credit = {isChecked: false};
     $scope.warn_amount = 1000;
     $scope.max_amount = 100000;
     $scope.amount_verified = false;
+    $scope.disable_auto_print = false;
+
+    // Load persistant settings
+    egCore.hatch.getItem('circ.bills.receiptonpay')
+                .then(function(rcptOnPay){
+                    if (rcptOnPay) $scope.receipt_on_pay.isChecked = rcptOnPay;
+                });
+
+    egCore.hatch.getItem('eg.circ.bills.annotatepayment')
+                .then(function(annoPay){
+                    if (annoPay) $scope.annotate_payment = annoPay;
+                });
 
     // pre-define list-returning funcs in case we access them
     // before the grid instantiates
@@ -175,6 +228,39 @@ function($scope , $q , $routeParams , egCore , egConfirmDialog , $location,
             return ['xact_start']; 
         }
     }
+    // -------------
+    // Apply coloring to rows based on fines stop reason
+    $scope.colorizeBillsList = {
+        apply: function(item) {
+            if (item['circulation.due_date'] && !item['circulation.checkin_time']) {
+                if (item['circulation.stop_fines'] == 'LOST') {
+                    return 'lost-row';
+                } else if (item['circulation.stop_fines'] == 'LONGOVERDUE') {
+                    return 'longoverdue-row';
+                } else {
+                    return 'overdue-row';
+                }
+            }
+        }
+    }
+
+    // Status Icon Column definition
+    $scope.statusIconColumn = {
+        isEnabled: true,
+        template: function(item) {
+            var icon = '';
+            if (item['circulation.due_date'] && !item['circulation.checkin_time']) {
+                if (item['circulation.stop_fines'] == "LOST") {
+                    icon = 'glyphicon-question-sign';
+                } else if (item['circulation.stop_fines'] == "LONGOVERDUE") {
+                    icon = 'glyphicon-exclamation-sign';
+                } else {
+                    icon = 'glyphicon-time';
+                }
+            }
+            return "<i class='glyphicon " + icon + "'></i>"
+        }
+    }
 
     billSvc.fetchSummary().then(function(s) {$scope.summary = s});
 
@@ -297,23 +383,42 @@ function($scope , $q , $routeParams , egCore , egConfirmDialog , $location,
 
     // generates payments, collects user note if needed, and sends payment
     // to server.
-    function sendPayment(note) {
+    function sendPayment(note, cc_args) {
+        $scope.applyingPayment = true;
         var make_payments = generatePayments();
-        billSvc.applyPayment(
-            $scope.payment_type, make_payments, note, $scope.check_number)
-        .then(function(payment_ids) {
+        var patron_credit = $scope.convert_to_credit.isChecked ?
+            $scope.pending_change() : 0; 
+        billSvc.applyPayment($scope.payment_type, 
+            make_payments, note, $scope.check_number, cc_args, patron_credit)
+        .then(
+            function(payment_ids) {
+
+                if (!$scope.disable_auto_print && $scope.receipt_on_pay.isChecked) {
+                    printReceipt(
+                        $scope.payment_type, payment_ids, make_payments, note);
+                }
 
-            if ($scope.receipt_on_pay) {
-                printReceipt(
-                    $scope.payment_type, payment_ids, make_payments, note);
+                refreshDisplay();
+            },
+            function(msg) {
+                console.error('Payment was rejected: ' + msg);
             }
+        )
+        .finally(function() { $scope.applyingPayment = false; })
+    }
+
+    $scope.onReceiptOnPayChanged = function(){
+        egCore.hatch.setItem('circ.bills.receiptonpay', $scope.receipt_on_pay.isChecked);
+    }
 
-            refreshDisplay();
-        })
+    $scope.onAnnotatePaymentChanged = function(){
+        egCore.hatch.setItem('eg.circ.bills.annotatepayment', $scope.annotate_payment);
     }
 
     function printReceipt(type, payment_ids, payments_made, note) {
         var payment_blobs = [];
+        var cusr = patronSvc.current;
+
         angular.forEach(payments_made, function(payment) {
             var xact_id = payment[0];
 
@@ -331,6 +436,7 @@ function($scope , $q , $routeParams , egCore , egConfirmDialog , $location,
 
         // page data not yet refreshed, capture data from current scope
         var print_data = {
+            payment_type : type,
             payment_note : note,
             previous_balance : Number($scope.summary.balance_owed()),
             payment_total : Number($scope.payment_amount),
@@ -340,7 +446,26 @@ function($scope , $q , $routeParams , egCore , egConfirmDialog , $location,
             payments : payment_blobs,
             current_location : egCore.idl.toHash(
                 egCore.org.get(egCore.auth.user().ws_ou()))
-        }
+        };
+
+        // Not a good idea to use patron_stats.fines for this; it's out of date
+        print_data.patron = {
+            prefix : cusr.prefix(),
+            first_given_name : cusr.first_given_name(),
+            second_given_name : cusr.second_given_name(),
+            family_name : cusr.family_name(),
+            suffix : cusr.suffix(),
+            pref_prefix : cusr.pref_prefix(),
+            pref_first_given_name : cusr.pref_first_given_name(),
+            pref_second_given_name : cusr.pref_second_given_name(),
+            pref_family_name : cusr.pref_family_name(),
+            pref_suffix : cusr.pref_suffix(),
+            card : { barcode : cusr.card().barcode() },
+            expire_date : cusr.expire_date(),
+            alias : cusr.alias(),
+            has_email : Boolean(patronSvc.current.email() && patronSvc.current.email().match(/.*@.*/)),
+            has_phone : Boolean(cusr.day_phone() || cusr.evening_phone() || cusr.other_phone())
+        };
 
         print_data.new_balance = (
             print_data.previous_balance * 100 - 
@@ -408,6 +533,14 @@ function($scope , $q , $routeParams , egCore , egConfirmDialog , $location,
         if (s['ui.circ.billing.amount_limit']) {
             $scope.max_amount = Number(s['ui.circ.billing.amount_limit']);
         }
+        if (s['circ.staff_client.do_not_auto_attempt_print'] && angular.isArray(s['circ.staff_client.do_not_auto_attempt_print'])) {
+            $scope.disable_auto_print = Boolean(
+                s['circ.staff_client.do_not_auto_attempt_print'].indexOf('Bill Pay') > -1
+            );
+        }
+        if (s['circ.disable_patron_credit']) {
+            $scope.disablePatronCredit = true;
+        }
     });
 
     $scope.gridControls.allItemsRetrieved = function() {
@@ -434,23 +567,64 @@ function($scope , $q , $routeParams , egCore , egConfirmDialog , $location,
         var xacts = [];
         egCore.pcrud.search('mbt', 
             {id : ids},
-            {flesh : 1, flesh_fields : {'mbt' : ['summary']}},
+            {flesh : 5, flesh_fields : {
+                'mbt' : ['summary', 'circulation'],
+                'circ' : ['target_copy'],
+                'acp' : ['call_number'],
+                'acn' : ['record'],
+                'bre' : ['simple_record']
+                }
+            },
             {authoritative : true}
         ).then(
             function() {
+                var cusr = patronSvc.current;
                 egCore.print.print({
                     context : 'receipt', 
                     template : 'bills_current', 
                     scope : {   
                         transactions : xacts,
                         current_location : egCore.idl.toHash(
-                            egCore.org.get(egCore.auth.user().ws_ou()))
+                            egCore.org.get(egCore.auth.user().ws_ou())),
+                        patron : {
+                            prefix : cusr.prefix(),
+                            first_given_name : cusr.first_given_name(),
+                            second_given_name : cusr.second_given_name(),
+                            family_name : cusr.family_name(),
+                            suffix : cusr.suffix(),
+                            pref_prefix : cusr.pref_prefix(),
+                            pref_first_given_name : cusr.pref_first_given_name(),
+                            pref_second_given_name : cusr.pref_second_given_name(),
+                            pref_family_name : cusr.pref_family_name(),
+                            pref_suffix : cusr.pref_suffix(),
+                            card : { barcode : cusr.card().barcode() },
+                            expire_date : cusr.expire_date(),
+                            alias : cusr.alias(),
+                            has_email : Boolean(cusr.email() && cusr.email().match(/.*@.*/)),
+                            has_phone : Boolean(cusr.day_phone() || cusr.evening_phone() || cusr.other_phone())
+                        }
                     }
                 });
             }, 
             null, 
             function(xact) {
-                xacts.push(egCore.idl.toHash(xact));
+                newXact = {
+                    billing_total : xact.billing_total(),
+                    billings : xact.billings(),
+                    grocery : xact.grocery(),
+                    id : xact.id(),
+                    payment_total : xact.payment_total(),
+                    payments : xact.payments(),
+                    summary : egCore.idl.toHash(xact.summary()),
+                    unrecovered : xact.unrecovered(),
+                    xact_finish : xact.xact_finish(),
+                    xact_start : xact.xact_start(),
+                }
+                if (xact.circulation()) {
+                    newXact.copy_barcode = xact.circulation().target_copy().barcode(),
+                    newXact.title = xact.circulation().target_copy().call_number().record().simple_record().title()
+                }
+                xacts.push(newXact);
             }
         );
     }
@@ -469,62 +643,146 @@ function($scope , $q , $routeParams , egCore , egConfirmDialog , $location,
             return;
         }
 
-        if (($scope.payment_amount > $scope.warn_amount) && ($scope.amount_verified == false)) {
-            egConfirmDialog.open(
-                egCore.strings.PAYMENT_WARN_AMOUNT_TITLE, egCore.strings.PAYMENT_WARN_AMOUNT,
-                {   payment_amount : ''+$scope.payment_amount,
-                    ok : function() {
-                        $scope.amount_verfied = true;
-                        $scope.applyPayment();
-                    },
-                    cancel : function() {
-                        $scope.payment_amount = 0;
+        verify_payment_amount().then(
+            function() { // amount confirmed
+                add_payment_note().then(function(pay_note) {
+                    add_cc_args().then(function(cc_args) {
+                        var note_text = pay_note ? pay_note.value || '' : null;
+                        sendPayment(note_text, cc_args);
+                    })
+                });
+            },
+            function() { // amount rejected
+                console.warn('payment amount rejected');
+                $scope.payment_amount = 0;
+            }
+        );
+    }
+
+    function verify_payment_amount() {
+        if ($scope.payment_amount < $scope.warn_amount)
+            return $q.when();
+
+        return egConfirmDialog.open(
+            egCore.strings.PAYMENT_WARN_AMOUNT_TITLE, 
+            egCore.strings.PAYMENT_WARN_AMOUNT,
+            {payment_amount : ''+$scope.payment_amount}
+        ).result;
+    }
+
+    function add_payment_note() {
+        if (!$scope.annotate_payment) return $q.when();
+        return egPromptDialog.open(
+            egCore.strings.ANNOTATE_PAYMENT_MSG, '').result;
+    }
+
+    function add_cc_args() {
+        if ($scope.payment_type != 'credit_card_payment') 
+            return $q.when();
+
+        return $uibModal.open({
+            templateUrl : './circ/patron/t_cc_payment_dialog',
+            backdrop: 'static',
+            controller : [
+                        '$scope','$uibModalInstance',
+                function($scope , $uibModalInstance) {
+
+                    $scope.context = {
+                        cc : {
+                            where_process : '1', // internal=1 ; external=0
+                            type : 'VISA', // external only
+                            billing_first : patronSvc.current.first_given_name(),
+                            billing_last : patronSvc.current.family_name()
+                        }
                     }
-                }
-            );
-            return;
-        }
 
-        $scope.amount_verfied = false;
+                    var addr = patronSvc.current.billing_address() ||
+                        patronSvc.current.mailing_address();
+                    if (addr) {
+                        var cc = $scope.context.cc;
+                        cc.billing_address = addr.street1() + 
+                            (addr.street2() ? ' ' + addr.street2() : '');
+                        cc.billing_city = addr.city();
+                        cc.billing_state = addr.state();
+                        cc.billing_zip = addr.post_code();
+                    }
 
-        if ($scope.annotate_payment) {
-            egPromptDialog.open(
-                egCore.strings.ANNOTATE_PAYMENT_MSG, '',
-                {ok : function(value) {sendPayment(value)}}
-            );
-        } else {
-            sendPayment();
-        }
+                    $scope.ok = function() {
+                        // CC payment form is not a <form>, 
+                        // so apply validation manually.
+                        if ( $scope.context.cc.where_process == 0 && 
+                            !$scope.context.cc.approval_code)
+                            return;
+
+                        $uibModalInstance.close($scope.context.cc);
+                    }
+
+                    $scope.cancel = function() {
+                        $uibModalInstance.dismiss();
+                    }
+                }
+            ]
+        }).result;
     }
 
     $scope.voidAllBillings = function(items) {
+        var promises = [];
+        var bill_ids = [];
+        var cents = 0;
         angular.forEach(items, function(item) {
+            promises.push(
+                billSvc.fetchBills(item.id).then(function(bills) {
+                    angular.forEach(bills, function(b) {
+                        if (b.voided() != 't') {
+                            cents += b.amount() * 100;
+                            bill_ids.push(b.id())
+                        }
+                    });
 
-            billSvc.fetchBills(item.id).then(function(bills) {
-                var bill_ids = [];
-                var cents = 0;
-                angular.forEach(bills, function(b) {
-                    if (b.voided() != 't') {
-                        cents += b.amount() * 100;
-                        bill_ids.push(b.id())
+                    if (bill_ids.length == 0) {
+                        // TODO: warn
+                        return;
                     }
-                });
 
-                $scope.session_voided = 
-                    ($scope.session_voided * 100 + cents) / 100;
+                })
+            );
+        });
 
-                if (bill_ids.length == 0) {
-                    // TODO: warn
-                    return;
+        $q.all(promises).then(function(){
+            egCore.audio.play('warning.circ.void_billings_confirmation');
+            egConfirmDialog.open(
+                egCore.strings.CONFIRM_VOID_BILLINGS, '', 
+                {   billIds : ''+bill_ids,
+                    amount : ''+(cents/100),
+                    ok : function() {
+                        billSvc.voidBills(bill_ids).then(function() {
+                            $scope.session_voided = 
+                                ($scope.session_voided * 100 + cents) / 100;
+                            refreshDisplay();
+                        });
+                    }
                 }
+            );
+        });
+    }
 
-                // TODO: alert of pending voiding
+    $scope.adjustToZero = function(items) {
+        if (items.length == 0) return;
+
+        var ids = items.map(function(item) {return item.id});
+
+        egCore.audio.play('warning.circ.adjust_to_zero_confirmation');
+        egConfirmDialog.open(
+            egCore.strings.CONFIRM_ADJUST_TO_ZERO, '', 
+            {   xactIds : ''+ids,
+                ok : function() {
+                    billSvc.adjustBillsToZero(ids).then(function() {
+                        refreshDisplay();
+                    });
+                }
+            }
+        );
 
-                billSvc.voidBills(bill_ids).then(function() {
-                    refreshDisplay();
-                });
-            });
-        });
     }
 
     // note this is functionally equivalent to selecting a neg. transaction
@@ -538,7 +796,8 @@ function($scope , $q , $routeParams , egCore , egConfirmDialog , $location,
         if (items.length == 0) return;
 
         var ids = items.map(function(item) {return item.id});
-            
+
+        egCore.audio.play('warning.circ.refund_confirmation');
         egConfirmDialog.open(
             egCore.strings.CONFIRM_REFUND_PAYMENT, '', 
             {   xactIds : ''+ids,
@@ -555,7 +814,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) {
@@ -568,11 +827,12 @@ function($scope , $q , $routeParams , egCore , egConfirmDialog , $location,
  * Displays details of a single transaction
  */
 .controller('XactDetailsCtrl',
-       ['$scope','$q','$routeParams','egCore','egGridDataProvider','patronSvc','billSvc','egPromptDialog','egBilling',
-function($scope,  $q , $routeParams , egCore , egGridDataProvider , patronSvc , billSvc , egPromptDialog , egBilling) {
+       ['$scope','$q','$routeParams','egCore','egGridDataProvider','patronSvc','billSvc','egPromptDialog','egBilling','egConfirmDialog',
+function($scope,  $q , $routeParams , egCore , egGridDataProvider , patronSvc , billSvc , egPromptDialog , egBilling , egConfirmDialog ) {
 
     $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} },
@@ -587,8 +847,12 @@ function($scope,  $q , $routeParams , egCore , egGridDataProvider , patronSvc ,
     // -- actions
     $scope.voidBillings = function(bill_list) {
         var bill_ids = [];
+        var cents = 0;
         angular.forEach(bill_list, function(b) {
-            if (b.voided != 't') bill_ids.push(b.id);
+            if (b.voided != 't') {
+                cents += b.amount * 100;
+                bill_ids.push(b.id)
+            }
         });
 
         if (bill_ids.length == 0) {
@@ -596,18 +860,28 @@ function($scope,  $q , $routeParams , egCore , egGridDataProvider , patronSvc ,
             return;
         }
 
-        billSvc.voidBills(bill_ids).then(function() {
+        egCore.audio.play('warning.circ.void_billings_confirmation');
+        egConfirmDialog.open(
+            egCore.strings.CONFIRM_VOID_BILLINGS, '', 
+            {   billIds : ''+bill_ids,
+                amount : ''+(cents/100),
+                ok : function() {
+                    billSvc.voidBills(bill_ids).then(function() {
+                        // TODO? $scope.session_voided = ...
 
-            // refresh bills and summary data
-            // note: no need to update payments
-            patronSvc.fetchUserStats();
+                        // refresh bills and summary data
+                        // note: no need to update payments
+                        patronSvc.fetchUserStats();
 
-            egBilling.fetchXact(xact_id).then(function(xact) {
-                $scope.xact = xact
-            });
+                        egBilling.fetchXact(xact_id).then(function(xact) {
+                            $scope.xact = xact
+                        });
 
-            xactGrid.refresh();
-        });
+                        xactGrid.refresh();
+                    });
+                }
+            }
+        );
     }
 
     // batch-edit billing and payment notes, depending on 'type'
@@ -645,6 +919,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;
@@ -758,7 +1039,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.
@@ -774,6 +1055,78 @@ function($scope,  $q , egCore , patronSvc , billSvc , egPromptDialog , $location
             })
         }
     }
+
+    $scope.printBills = function(selected) { // FIXME: refactor me
+        if (!selected.length) return;
+        // bills print receipt assumes nested hashes, but our grid
+        // stores flattener data.  Fetch the selected xacts as
+        // fleshed pcrud objects and hashify.  
+        // (Consider an alternate approach..)
+        var ids = selected.map(function(t){ return t.id });
+        var xacts = [];
+        egCore.pcrud.search('mbt', 
+            {id : ids},
+            {flesh : 5, flesh_fields : {
+                'mbt' : ['summary', 'circulation'],
+                'circ' : ['target_copy'],
+                'acp' : ['call_number'],
+                'acn' : ['record'],
+                'bre' : ['simple_record']
+                }
+            },
+            {authoritative : true}
+        ).then(
+            function() {
+                var cusr = patronSvc.current;
+                egCore.print.print({
+                    context : 'receipt', 
+                    template : 'bills_historical', 
+                    scope : {   
+                        transactions : xacts,
+                        current_location : egCore.idl.toHash(
+                            egCore.org.get(egCore.auth.user().ws_ou())),
+                        patron : {
+                            prefix : cusr.prefix(),
+                            pref_prefix : cusr.pref_prefix(),
+                            pref_first_given_name : cusr.pref_first_given_name(),
+                            pref_second_given_name : cusr.pref_second_given_name(),
+                            pref_family_name : cusr.pref_family_name(),
+                            pref_suffix : cusr.pref_suffix(),
+                            first_given_name : cusr.first_given_name(),
+                            second_given_name : cusr.second_given_name(),
+                            family_name : cusr.family_name(),
+                            suffix : cusr.suffix(),
+                            card : { barcode : cusr.card().barcode() },
+                            expire_date : cusr.expire_date(),
+                            alias : cusr.alias(),
+                            has_email : Boolean(cusr.email() && cusr.email().match(/.*@.*/)),
+                            has_phone : Boolean(cusr.day_phone() || cusr.evening_phone() || cusr.other_phone())
+                        }
+                    }
+                });
+            }, 
+            null, 
+            function(xact) {
+                newXact = {
+                    billing_total : xact.billing_total(),
+                    billings : xact.billings(),
+                    grocery : xact.grocery(),
+                    id : xact.id(),
+                    payment_total : xact.payment_total(),
+                    payments : xact.payments(),
+                    summary : egCore.idl.toHash(xact.summary()),
+                    unrecovered : xact.unrecovered(),
+                    xact_finish : xact.xact_finish(),
+                    xact_start : xact.xact_start(),
+                }
+                if (xact.circulation()) {
+                    newXact.copy_barcode = xact.circulation().target_copy().barcode(),
+                    newXact.title = xact.circulation().target_copy().call_number().record().simple_record().title()
+                }
+                xacts.push(newXact);
+            }
+        );
+    }
 }])
 
 .controller('BillPaymentHistoryCtrl',
@@ -807,7 +1160,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() {