LP#1676608: copy alert and suppression matrix
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / circ / services / circ.js
index edf975a..91ad8fb 100644 (file)
@@ -6,14 +6,41 @@ angular.module('egCoreMod')
 
 .factory('egCirc',
 
-       ['$modal','$q','egCore','egAlertDialog','egConfirmDialog',
-function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
+       ['$uibModal','$q','egCore','egAlertDialog','egConfirmDialog','egAddCopyAlertDialog','egCopyAlertManagerDialog','egCopyAlertEditorDialog',
+        'egWorkLog',
+function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAlertDialog , egCopyAlertManagerDialog,  egCopyAlertEditorDialog ,
+         egWorkLog) {
 
     var service = {
         // auto-override these events after the first override
         auto_override_checkout_events : {},
+        require_initials : false,
+        never_auto_print : {
+            hold_shelf_slip : false,
+            hold_transit_slip : false,
+            transit_slip : false
+        }
     };
 
+    egCore.startup.go().finally(function() {
+        egCore.org.settings([
+            'ui.staff.require_initials.patron_standing_penalty',
+            'ui.admin.work_log.max_entries',
+            'ui.admin.patron_log.max_entries',
+            'circ.staff_client.do_not_auto_attempt_print'
+        ]).then(function(set) {
+            service.require_initials = Boolean(set['ui.staff.require_initials.patron_standing_penalty']);
+            if (angular.isArray(set['circ.staff_client.do_not_auto_attempt_print'])) {
+                if (set['circ.staff_client.do_not_auto_attempt_print'].indexOf('Hold Slip') > 1)
+                    service.never_auto_print['hold_shelf_slip'] = true;
+                if (set['circ.staff_client.do_not_auto_attempt_print'].indexOf('Hold/Transit Slip') > 1)
+                    service.never_auto_print['hold_transit_slip'] = true;
+                if (set['circ.staff_client.do_not_auto_attempt_print'].indexOf('Transit Slip') > 1)
+                    service.never_auto_print['transit_slip'] = true;
+            }
+        });
+    });
+
     service.reset = function() {
         service.auto_override_checkout_events = {};
     }
@@ -75,6 +102,7 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         'CIRC_CLAIMS_RETURNED',
         'COPY_ALERT_MESSAGE',
         'COPY_STATUS_LOST',
+        'COPY_STATUS_LOST_AND_PAID',
         'COPY_STATUS_LONG_OVERDUE',
         'COPY_STATUS_MISSING',
         'PATRON_EXCEEDS_FINES'
@@ -83,6 +111,7 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     // these events can be overridden by staff during checkin
     service.checkin_overridable_events = 
         service.checkin_suppress_overrides.concat([
+        'HOLD_CAPTURE_DELAYED', // not technically overridable, but special prompt and param
         'TRANSIT_CHECKIN_INTERVAL_BLOCK'
     ])
 
@@ -95,35 +124,52 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     // options : non-parameter controls.  e.g. "override", "check_barcode"
     service.checkout = function(params, options) {
         if (!options) options = {};
+        params.new_copy_alerts = 1;
 
         console.debug('egCirc.checkout() : ' 
             + js2JSON(params) + ' : ' + js2JSON(options));
 
-        var promise = options.check_barcode ? 
-            service.test_barcode(params.copy_barcode) : $q.when();
+        // handle barcode completion
+        return service.handle_barcode_completion(params.copy_barcode)
+        .then(function(barcode) {
+            console.debug('barcode after completion: ' + barcode);
+            params.copy_barcode = barcode;
 
-        // avoid re-check on override, etc.
-        delete options.check_barcode;
+            var promise = options.check_barcode ? 
+                service.test_barcode(params.copy_barcode) : $q.when();
 
-        return promise.then(function() {
+            // avoid re-check on override, etc.
+            delete options.check_barcode;
 
-            var method = 'open-ils.circ.checkout.full';
-            if (options.override) method += '.override';
+            return promise.then(function() {
 
-            return egCore.net.request(
-                'open-ils.circ', method, egCore.auth.token(), params
+                var method = 'open-ils.circ.checkout.full';
+                if (options.override) method += '.override';
 
-            ).then(function(evt) {
+                return egCore.net.request(
+                    'open-ils.circ', method, egCore.auth.token(), params
 
-                if (angular.isArray(evt)) evt = evt[0];
+                ).then(function(evt) {
 
-                return service.flesh_response_data('checkout', evt, params, options)
-                .then(function() {
-                    return service.handle_checkout_resp(evt, params, options);
-                })
-                .then(function(final_resp) {
-                    return service.munge_resp_data(final_resp)
-                })
+                    if (!angular.isArray(evt)) evt = [evt];
+
+                    if (evt[0].payload && evt[0].payload.auto_renew == 1) {
+                        // open circulation found with auto-renew toggle on.
+                        console.debug('Auto-renewing item ' + params.copy_barcode);
+                        options.auto_renew = true;
+                        return service.renew(params, options);
+                    }
+
+                    var action = params.noncat ? 'noncat_checkout' : 'checkout';
+
+                    return service.flesh_response_data(action, evt, params, options)
+                    .then(function() {
+                        return service.handle_checkout_resp(evt, params, options);
+                    })
+                    .then(function(final_resp) {
+                        return service.munge_resp_data(final_resp,action,method)
+                    })
+                });
             });
         });
     }
@@ -134,36 +180,44 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     // Rejected if the renewal cannot be completed.
     service.renew = function(params, options) {
         if (!options) options = {};
+        params.new_copy_alerts = 1;
 
         console.debug('egCirc.renew() : ' 
             + js2JSON(params) + ' : ' + js2JSON(options));
 
-        var promise = options.check_barcode ? 
-            service.test_barcode(params.copy_barcode) : $q.when();
+        // handle barcode completion
+        return service.handle_barcode_completion(params.copy_barcode)
+        .then(function(barcode) {
+            params.copy_barcode = barcode;
 
-        // avoid re-check on override, etc.
-        delete options.check_barcode;
+            var promise = options.check_barcode ? 
+                service.test_barcode(params.copy_barcode) : $q.when();
 
-        return promise.then(function() {
+            // avoid re-check on override, etc.
+            delete options.check_barcode;
 
-            var method = 'open-ils.circ.renew';
-            if (options.override) method += '.override';
+            return promise.then(function() {
 
-            return egCore.net.request(
-                'open-ils.circ', method, egCore.auth.token(), params
+                var method = 'open-ils.circ.renew';
+                if (options.override) method += '.override';
 
-            ).then(function(evt) {
+                return egCore.net.request(
+                    'open-ils.circ', method, egCore.auth.token(), params
 
-                if (angular.isArray(evt)) evt = evt[0];
+                ).then(function(evt) {
 
-                return service.flesh_response_data(
-                    'renew', evt, params, options)
-                .then(function() {
-                    return service.handle_renew_resp(evt, params, options);
-                })
-                .then(function(final_resp) {
-                    return service.munge_resp_data(final_resp)
-                })
+                    if (!angular.isArray(evt)) evt = [evt];
+
+                    return service.flesh_response_data(
+                        'renew', evt, params, options)
+                    .then(function() {
+                        return service.handle_renew_resp(evt, params, options);
+                    })
+                    .then(function(final_resp) {
+                        final_resp.auto_renew = options.auto_renew;
+                        return service.munge_resp_data(final_resp,'renew',method)
+                    })
+                });
             });
         });
     }
@@ -174,62 +228,86 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     // Rejected if the checkin cannot be completed.
     service.checkin = function(params, options) {
         if (!options) options = {};
+        params.new_copy_alerts = 1;
 
         console.debug('egCirc.checkin() : ' 
             + js2JSON(params) + ' : ' + js2JSON(options));
 
-        var promise = options.check_barcode ? 
-            service.test_barcode(params.copy_barcode) : $q.when();
+        // handle barcode completion
+        return service.handle_barcode_completion(params.copy_barcode)
+        .then(function(barcode) {
+            params.copy_barcode = barcode;
 
-        // avoid re-check on override, etc.
-        delete options.check_barcode;
+            var promise = options.check_barcode ? 
+                service.test_barcode(params.copy_barcode) : $q.when();
 
-        return promise.then(function() {
+            // avoid re-check on override, etc.
+            delete options.check_barcode;
 
-            var method = 'open-ils.circ.checkin';
-            if (options.override) method += '.override';
+            return promise.then(function() {
 
-            return egCore.net.request(
-                'open-ils.circ', method, egCore.auth.token(), params
+                var method = 'open-ils.circ.checkin';
+                if (options.override) method += '.override';
 
-            ).then(function(evt) {
+                return egCore.net.request(
+                    'open-ils.circ', method, egCore.auth.token(), params
 
-                if (angular.isArray(evt)) evt = evt[0];
-                return service.flesh_response_data(
-                    'checkin', evt, params, options)
-                .then(function() {
-                    return service.handle_checkin_resp(evt, params, options);
-                })
-                .then(function(final_resp) {
-                    return service.munge_resp_data(final_resp)
-                })
+                ).then(function(evt) {
+
+                    if (!angular.isArray(evt)) evt = [evt];
+                    return service.flesh_response_data(
+                        'checkin', evt, params, options)
+                    .then(function() {
+                        return service.handle_checkin_resp(evt, params, options);
+                    })
+                    .then(function(final_resp) {
+                        return service.munge_resp_data(final_resp,'checkin',method)
+                    })
+                });
             });
         });
     }
 
     // provide consistent formatting of the final response data
-    service.munge_resp_data = function(final_resp) {
+    service.munge_resp_data = function(final_resp,worklog_action,worklog_method) {
         var data = final_resp.data = {};
 
-        if (!final_resp.evt) return;
+        if (!final_resp.evt[0]) {
+            egCore.audio.play('error.unknown.no_event');
+            return;
+        }
+
+        var payload = final_resp.evt[0].payload;
+        if (!payload) {
+            egCore.audio.play('error.unknown.no_payload');
+            return;
+        }
 
-        var payload = final_resp.evt.payload;
-        if (!payload) return;
+        // retrieve call number affixes prior to sending payload data to the grid
+        if (payload.volume && typeof payload.volume.prefix() != 'object') {
+            egCore.pcrud.retrieve('acnp',payload.volume.prefix()).then(function(p) {payload.volume.prefix(p)});
+        };
+        if (payload.volume && typeof payload.volume.suffix() != 'object') {
+            egCore.pcrud.retrieve('acns',payload.volume.suffix()).then(function(s) {payload.volume.suffix(s)});
+        };
 
         data.circ = payload.circ;
         data.parent_circ = payload.parent_circ;
         data.hold = payload.hold;
         data.record = payload.record;
         data.acp = payload.copy;
-        data.acn = payload.volume ?  payload.volume : payload.copy.call_number();
+        data.acn = payload.volume ?  payload.volume : payload.copy ? payload.copy.call_number() : null;
         data.au = payload.patron;
         data.transit = payload.transit;
         data.status = payload.status;
         data.message = payload.message;
-        data.title = final_resp.evt.title;
-        data.author = final_resp.evt.author;
-        data.isbn = final_resp.evt.isbn;
-        data.route_to = final_resp.evt.route_to;
+        data.title = final_resp.evt[0].title;
+        data.author = final_resp.evt[0].author;
+        data.isbn = final_resp.evt[0].isbn;
+        data.route_to = final_resp.evt[0].route_to;
+
+        if (payload.circ) data.duration = payload.circ.duration();
+        if (payload.circ) data.circ_lib = payload.circ.circ_lib();
 
         // for checkin, the mbts lives on the main circ
         if (payload.circ && payload.circ.billable_transaction())
@@ -247,6 +325,19 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
             }
         }
 
+        egWorkLog.record(
+            (worklog_action == 'checkout' || worklog_action == 'noncat_checkout')
+            ? egCore.strings.EG_WORK_LOG_CHECKOUT
+            : (worklog_action == 'renew'
+                ? egCore.strings.EG_WORK_LOG_RENEW
+                : egCore.strings.EG_WORK_LOG_CHECKIN // worklog_action == 'checkin'
+            ),{
+                'action' : worklog_action,
+                'method' : worklog_method,
+                'response' : final_resp
+            }
+        );
+
         return final_resp;
     }
 
@@ -256,14 +347,14 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
             // override attempt already made and failed.
             // NOTE: I don't think we'll ever get here, since the
             // override attempt should produce a perm failure...
-            console.debug('override failed: ' + evt.textcode);
+            angular.forEach(evt, function(e){ console.debug('override failed: ' + e.textcode); });
             return $q.reject();
 
         } 
 
-        if (service.auto_override_checkout_events[evt.textcode]) {
-            // user has already opted to override this type
-            // of event.  Re-run the checkout w/ override.
+        if (evt.filter(function(e){return !service.auto_override_checkout_events[e.textcode];}).length == 0) {
+            // user has already opted to override these type
+            // of events.  Re-run the checkout w/ override.
             options.override = true;
             return service.checkout(params, options);
         } 
@@ -272,11 +363,11 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         // Some events offer a stock override dialog, while others
         // require additional context.
 
-        switch(evt.textcode) {
+        switch(evt[0].textcode) {
             case 'COPY_NOT_AVAILABLE':
-                return service.copy_not_avail_dialog(evt, params, options);
+                return service.copy_not_avail_dialog(evt[0], params, options);
             case 'COPY_ALERT_MESSAGE':
-                return service.copy_alert_dialog(evt, params, options, 'checkout');
+                return service.copy_alert_dialog(evt[0], params, options, 'checkout');
             default: 
                 return service.override_dialog(evt, params, options, 'checkout');
         }
@@ -288,15 +379,15 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
             // override attempt already made and failed.
             // NOTE: I don't think we'll ever get here, since the
             // override attempt should produce a perm failure...
-            console.debug('override failed: ' + evt.textcode);
+            angular.forEach(evt, function(e){ console.debug('override failed: ' + e.textcode); });
             return $q.reject();
 
         } 
 
         // renewal auto-overrides are the same as checkout
-        if (service.auto_override_checkout_events[evt.textcode]) {
-            // user has already opted to override this type
-            // of event.  Re-run the renew w/ override.
+        if (evt.filter(function(e){return !service.auto_override_checkout_events[e.textcode];}).length == 0) {
+            // user has already opted to override these type
+            // of events.  Re-run the renew w/ override.
             options.override = true;
             return service.renew(params, options);
         } 
@@ -305,9 +396,9 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         // Some events offer a stock override dialog, while others
         // require additional context.
 
-        switch(evt.textcode) {
+        switch(evt[0].textcode) {
             case 'COPY_ALERT_MESSAGE':
-                return service.copy_alert_dialog(evt, params, options, 'renew');
+                return service.copy_alert_dialog(evt[0], params, options, 'renew');
             default: 
                 return service.override_dialog(evt, params, options, 'renew');
         }
@@ -320,14 +411,14 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
             // override attempt already made and failed.
             // NOTE: I don't think we'll ever get here, since the
             // override attempt should produce a perm failure...
-            console.debug('override failed: ' + evt.textcode);
+            angular.forEach(evt, function(e){ console.debug('override failed: ' + e.textcode); });
             return $q.reject();
 
         } 
 
         if (options.suppress_checkin_popups
-            && service.checkin_suppress_overrides.indexOf(evt.textcode) > -1) {
-            // Event is suppressed.  Re-run the checkin w/ override.
+            && evt.filter(function(e){return service.checkin_suppress_overrides.indexOf(e.textcode) == -1;}).length == 0) {
+            // Events are suppressed.  Re-run the checkin w/ override.
             options.override = true;
             return service.checkin(params, options);
         } 
@@ -336,9 +427,11 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         // Some events offer a stock override dialog, while others
         // require additional context.
 
-        switch(evt.textcode) {
+        switch(evt[0].textcode) {
             case 'COPY_ALERT_MESSAGE':
-                return service.copy_alert_dialog(evt, params, options, 'checkin');
+                return service.copy_alert_dialog(evt[0], params, options, 'checkin');
+            case 'HOLD_CAPTURE_DELAYED':
+                return service.hold_capture_delay_dialog(evt[0], params, options, 'checkin');
             default: 
                 return service.override_dialog(evt, params, options, 'checkin');
         }
@@ -350,15 +443,16 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         var final_resp = {evt : evt, params : params, options : options};
 
         // track the barcode regardless of whether it refers to a copy
-        evt.copy_barcode = params.copy_barcode;
+        angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
 
         // Overridable Events
-        if (service.renew_overridable_events.indexOf(evt.textcode) > -1) 
+        if (evt.filter(function(e){return service.renew_overridable_events.indexOf(e.textcode) > -1;}).length > 0)
             return service.handle_overridable_renew_event(evt, params, options);
 
         // Other events
-        switch (evt.textcode) {
+        switch (evt[0].textcode) {
             case 'SUCCESS':
+                egCore.audio.play('info.renew');
                 return $q.when(final_resp);
 
             case 'COPY_IN_TRANSIT':
@@ -366,23 +460,19 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
             case 'PATRON_INACTIVE':
             case 'PATRON_ACCOUNT_EXPIRED':
             case 'CIRC_CLAIMS_RETURNED':
+                egCore.audio.play('warning.renew');
                 return service.exit_alert(
-                    egCore.strings[evt.textcode],
+                    egCore.strings[evt[0].textcode],
                     {barcode : params.copy_barcode}
                 );
 
-            case 'PERM_FAILURE':
-                return service.exit_alert(
-                    egCore.strings[evt.textcode],
-                    {permission : evt.ilsperm}
-                );
-
             default:
+                egCore.audio.play('warning.renew.unknown');
                 return service.exit_alert(
                     egCore.strings.CHECKOUT_FAILED_GENERIC, {
                         barcode : params.copy_barcode,
-                        textcode : evt.textcode,
-                        desc : evt.desc
+                        textcode : evt[0].textcode,
+                        desc : evt[0].desc
                     }
                 );
         }
@@ -394,47 +484,48 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         var final_resp = {evt : evt, params : params, options : options};
 
         // track the barcode regardless of whether it refers to a copy
-        evt.copy_barcode = params.copy_barcode;
+        angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
 
         // Overridable Events
-        if (service.checkout_overridable_events.indexOf(evt.textcode) > -1) 
+        if (evt.filter(function(e){return service.checkout_overridable_events.indexOf(e.textcode) > -1;}).length > 0)
             return service.handle_overridable_checkout_event(evt, params, options);
 
         // Other events
-        switch (evt.textcode) {
+        switch (evt[0].textcode) {
             case 'SUCCESS':
+                egCore.audio.play('success.checkout');
                 return $q.when(final_resp);
 
             case 'ITEM_NOT_CATALOGED':
+                egCore.audio.play('error.checkout.no_cataloged');
                 return service.precat_dialog(params, options);
 
             case 'OPEN_CIRCULATION_EXISTS':
+                // auto_renew checked in service.checkout()
+                egCore.audio.play('error.checkout.open_circ');
                 return service.circ_exists_dialog(evt, params, options);
 
             case 'COPY_IN_TRANSIT':
+                egCore.audio.play('warning.checkout.in_transit');
                 return service.copy_in_transit_dialog(evt, params, options);
 
             case 'PATRON_CARD_INACTIVE':
             case 'PATRON_INACTIVE':
             case 'PATRON_ACCOUNT_EXPIRED':
             case 'CIRC_CLAIMS_RETURNED':
+                egCore.audio.play('warning.checkout');
                 return service.exit_alert(
-                    egCore.strings[evt.textcode],
+                    egCore.strings[evt[0].textcode],
                     {barcode : params.copy_barcode}
                 );
 
-            case 'PERM_FAILURE':
-                return service.exit_alert(
-                    egCore.strings[evt.textcode],
-                    {permission : evt.ilsperm}
-                );
-
             default:
+                egCore.audio.play('error.checkout.unknown');
                 return service.exit_alert(
                     egCore.strings.CHECKOUT_FAILED_GENERIC, {
                         barcode : params.copy_barcode,
-                        textcode : evt.textcode,
-                        desc : evt.desc
+                        textcode : evt[0].textcode,
+                        desc : evt[0].desc
                     }
                 );
         }
@@ -483,10 +574,13 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     service.flesh_response_data = function(action, evt, params, options) {
         var promises = [];
         var payload;
-        if (!evt || !(payload = evt.payload)) return $q.when();
-
+        if (!evt[0] || !(payload = evt[0].payload)) return $q.when();
+        
         promises.push(service.flesh_copy_location(payload.copy));
         if (payload.copy) {
+            promises.push(service.flesh_acn_owning_lib(payload.volume));
+            promises.push(service.flesh_copy_circ_library(payload.copy));
+            promises.push(service.flesh_copy_circ_modifier(payload.copy));
             promises.push(
                 service.flesh_copy_status(payload.copy)
 
@@ -515,26 +609,55 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         } 
 
         // TODO: renewal responses should include the patron
-        if (!payload.patron && payload.circ) {
-            promises.push(
-                egCore.pcrud.retrieve('au', payload.circ.usr())
-                .then(function(user) {payload.patron = user})
-            );
+        if (!payload.patron) {
+            var user_id;
+            if (payload.circ) user_id = payload.circ.usr();
+            if (payload.noncat_circ) user_id = payload.noncat_circ.patron();
+            if (user_id) {
+                promises.push(
+                    egCore.pcrud.retrieve('au', user_id)
+                    .then(function(user) {payload.patron = user})
+                );
+            }
         }
 
         // extract precat values
-        evt.title = payload.record ? payload.record.title() : 
-            (payload.copy ? payload.copy.dummy_title() : null);
+        angular.forEach(evt, function(e){ e.title = payload.record ? payload.record.title() : 
+            (payload.copy ? payload.copy.dummy_title() : null);});
 
-        evt.author = payload.record ? payload.record.author() : 
-            (payload.copy ? payload.copy.dummy_author() : null);
+        angular.forEach(evt, function(e){ e.author = payload.record ? payload.record.author() : 
+            (payload.copy ? payload.copy.dummy_author() : null);});
 
-        evt.isbn = payload.record ? payload.record.isbn() : 
-            (payload.copy ? payload.copy.dummy_isbn() : null);
+        angular.forEach(evt, function(e){ e.isbn = payload.record ? payload.record.isbn() : 
+            (payload.copy ? payload.copy.dummy_isbn() : null);});
 
         return $q.all(promises);
     }
 
+    service.flesh_acn_owning_lib = function(acn) {
+        if (!acn) return $q.when();
+        return $q.when(acn.owning_lib(egCore.org.get( acn.owning_lib() )));
+    }
+
+    service.flesh_copy_circ_library = function(copy) {
+        if (!copy) return $q.when();
+        
+        return $q.when(copy.circ_lib(egCore.org.get( copy.circ_lib() )));
+    }
+
+    // fetches the full list of circ modifiers
+    service.flesh_copy_circ_modifier = function(copy) {
+        if (!copy) return $q.when();
+        if (egCore.env.ccm)
+            return $q.when(copy.circ_modifier(egCore.env.ccm.map[copy.circ_modifier()]));
+        return egCore.pcrud.retrieveAll('ccm', {}, {atomic : true}).then(
+            function(list) {
+                egCore.env.absorbList(list, 'ccm');
+                copy.circ_modifier(egCore.env.ccm.map[copy.circ_modifier()]);
+            }
+        );
+    }
+
     // fetches the full list of copy statuses
     service.flesh_copy_status = function(copy) {
         if (!copy) return $q.when();
@@ -573,6 +696,8 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         var org = egCore.org.get(org_id);
         var addr_id = org[addr_type]();
 
+        if (!addr_id) return $q.when(null);
+
         if (egCore.env.aoa && egCore.env.aoa.map[addr_id]) 
             return $q.when(egCore.env.aoa.map[addr_id]); 
 
@@ -590,18 +715,24 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     // opens a dialog asking the user if they would like to override
     // the returned event.
     service.override_dialog = function(evt, params, options, action) {
-        return $modal.open({
+        if (!angular.isArray(evt)) evt = [evt];
+
+        egCore.audio.play('warning.circ.event_override');
+        return $uibModal.open({
             templateUrl: './circ/share/t_event_override_dialog',
+            backdrop: 'static',
             controller: 
-                ['$scope', '$modalInstance', 
-                function($scope, $modalInstance) {
-                $scope.evt = evt;
-                $scope.auto_override = 
-                    service.checkout_auto_override_after_first.indexOf(evt.textcode) > -1;
+                ['$scope', '$uibModalInstance', 
+                function($scope, $uibModalInstance) {
+                $scope.events = evt;
+                $scope.auto_override =
+                    evt.filter(function(e){
+                        return service.checkout_auto_override_after_first.indexOf(evt.textcode) > -1;
+                    }).length > 0;
                 $scope.copy_barcode = params.copy_barcode; // may be null
-                $scope.ok = function() { $modalInstance.close() }
+                $scope.ok = function() { $uibModalInstance.close() }
                 $scope.cancel = function ($event) { 
-                    $modalInstance.dismiss();
+                    $uibModalInstance.dismiss();
                     $event.preventDefault();
                 }
             }]
@@ -614,8 +745,10 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
                 }
 
                 // checkout/renew support override-after-first
-                if (service.checkout_auto_override_after_first.indexOf(evt.textcode) > -1)
-                    service.auto_override_checkout_events[evt.textcode] = true;
+                angular.forEach(evt, function(e){
+                    if (service.checkout_auto_override_after_first.indexOf(e.textcode) > -1)
+                        service.auto_override_checkout_events[e.textcode] = true;
+                });
 
                 return service[action](params, options);
             }
@@ -623,14 +756,16 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     }
 
     service.copy_not_avail_dialog = function(evt, params, options) {
-        return $modal.open({
+        if (angular.isArray(evt)) evt = evt[0];
+        return $uibModal.open({
             templateUrl: './circ/share/t_copy_not_avail_dialog',
+            backdrop: 'static',
             controller: 
-                       ['$scope','$modalInstance','copyStatus',
-                function($scope , $modalInstance , copyStatus) {
+                       ['$scope','$uibModalInstance','copyStatus',
+                function($scope , $uibModalInstance , copyStatus) {
                 $scope.copyStatus = copyStatus;
-                $scope.ok = function() {$modalInstance.close()}
-                $scope.cancel = function() {$modalInstance.dismiss()}
+                $scope.ok = function() {$uibModalInstance.close()}
+                $scope.cancel = function() {$uibModalInstance.dismiss()}
             }],
             resolve : {
                 copyStatus : function() {
@@ -663,18 +798,19 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
             params.noncat = true;
             var type = egCore.env.cnct.map[params.noncat_type];
 
-            return $modal.open({
+            return $uibModal.open({
                 templateUrl: './circ/share/t_noncat_dialog',
+                backdrop: 'static',
                 controller: 
-                    ['$scope', '$modalInstance',
-                    function($scope, $modalInstance) {
+                    ['$scope', '$uibModalInstance',
+                    function($scope, $uibModalInstance) {
                     $scope.focusMe = true;
                     $scope.type = type;
                     $scope.count = 1;
                     $scope.noncatMax = noncatMax;
-                    $scope.ok = function(count) { $modalInstance.close(count) }
+                    $scope.ok = function(count) { $uibModalInstance.close(count) }
                     $scope.cancel = function ($event) { 
-                        $modalInstance.dismiss() 
+                        $uibModalInstance.dismiss() 
                         $event.preventDefault();
                     }
                 }],
@@ -695,19 +831,26 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     // Opens a dialog allowing the user to fill in pre-cat copy info.
     service.precat_dialog = function(params, options) {
 
-        return $modal.open({
+        return $uibModal.open({
             templateUrl: './circ/share/t_precat_dialog',
+            backdrop: 'static',
             controller: 
-                ['$scope', '$modalInstance', 'circMods',
-                function($scope, $modalInstance, circMods) {
+                ['$scope', '$uibModalInstance', 'circMods',
+                function($scope, $uibModalInstance, circMods) {
                 $scope.focusMe = true;
                 $scope.precatArgs = {
-                    copy_barcode : params.copy_barcode,
-                    circ_modifier : circMods.length ? circMods[0].code() : null
+                    copy_barcode : params.copy_barcode
                 };
                 $scope.circModifiers = circMods;
-                $scope.ok = function(args) { $modalInstance.close(args) }
-                $scope.cancel = function () { $modalInstance.dismiss() }
+                $scope.ok = function(args) { $uibModalInstance.close(args) }
+                $scope.cancel = function () { $uibModalInstance.dismiss() }
+
+                // use this function as a keydown handler on form
+                // elements that should not submit the form on enter.
+                $scope.preventSubmit = function($event) {
+                    if ($event.keyCode == 13)
+                        $event.preventDefault();
+                }
             }],
             resolve : {
                 circMods : function() { 
@@ -717,6 +860,7 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         }).result.then(
             function(args) {
                 if (!args || !args.dummy_title) return $q.reject();
+                if(args.circ_modifier == "") args.circ_modifier = null;
                 angular.forEach(args, function(val, key) {params[key] = val});
                 params.precat = true;
                 return service.checkout(params, options);
@@ -727,6 +871,7 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     // find the open transit for the given copy barcode; flesh the org
     // units locally.
     service.find_copy_transit = function(evt, params, options) {
+        if (angular.isArray(evt)) evt = evt[0];
 
         if (evt && evt.payload && evt.payload.transit)
             return $q.when(evt.payload.transit);
@@ -754,14 +899,16 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     }
 
     service.copy_in_transit_dialog = function(evt, params, options) {
-        return $modal.open({
+        if (angular.isArray(evt)) evt = evt[0];
+        return $uibModal.open({
             templateUrl: './circ/share/t_copy_in_transit_dialog',
+            backdrop: 'static',
             controller: 
-                       ['$scope','$modalInstance','transit',
-                function($scope , $modalInstance , transit) {
+                       ['$scope','$uibModalInstance','transit',
+                function($scope , $uibModalInstance , transit) {
                 $scope.transit = transit;
-                $scope.ok = function() { $modalInstance.close(transit) }
-                $scope.cancel = function() { $modalInstance.dismiss() }
+                $scope.ok = function() { $uibModalInstance.close(transit) }
+                $scope.cancel = function() { $uibModalInstance.dismiss() }
             }],
             resolve : {
                 // fetch the conflicting open transit w/ fleshed copy
@@ -802,33 +949,69 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     }
 
     service.circ_exists_dialog = function(evt, params, options) {
+        if (angular.isArray(evt)) evt = evt[0];
+
+        if (!evt.payload.old_circ) {
+            return egCore.net.request(
+                'open-ils.search',
+                'open-ils.search.asset.copy.fleshed2.find_by_barcode',
+                params.copy_barcode
+            ).then(function(resp){
+                console.log(resp);
+                if (egCore.evt.parse(resp)) {
+                    console.error(egCore.evt.parse(resp));
+                } else {
+                    return egCore.net.request(
+                         'open-ils.circ',
+                         'open-ils.circ.copy_checkout_history.retrieve',
+                         egCore.auth.token(), resp.id(), 1
+                    ).then( function (circs) {
+                        evt.payload.old_circ = circs[0];
+                        return service.circ_exists_dialog_impl( evt, params, options );
+                    });
+                }
+            });
+        } else {
+            return service.circ_exists_dialog_impl( evt, params, options );
+        }
+    },
+
+    service.circ_exists_dialog_impl = function (evt, params, options) {
 
         var openCirc = evt.payload.old_circ;
         var sameUser = openCirc.usr() == params.patron_id;
         
-        return $modal.open({
+        return $uibModal.open({
             templateUrl: './circ/share/t_circ_exists_dialog',
+            backdrop: 'static',
             controller: 
-                       ['$scope','$modalInstance',
-                function($scope , $modalInstance) {
+                       ['$scope','$uibModalInstance',
+                function($scope , $uibModalInstance) {
+                $scope.args = {forgive_fines : false};
                 $scope.circDate = openCirc.xact_start();
                 $scope.sameUser = sameUser;
-                $scope.ok = function() { $modalInstance.close() }
+                $scope.ok = function() { $uibModalInstance.close($scope.args) }
                 $scope.cancel = function($event) { 
-                    $modalInstance.dismiss();
+                    $uibModalInstance.dismiss();
                     $event.preventDefault(); // form, avoid calling ok();
                 }
             }]
         }).result.then(
-            function() {
-                
-                return service.checkin(
-                    {barcode : params.copy_barcode, noop : true}
-                ).then(function(checkin_resp) {
-                    if (checkin_resp.evt.textcode == 'SUCCESS') {
+            function(args) {
+                if (sameUser) {
+                    params.void_overdues = args.forgive_fines;
+                    return service.renew(params, options);
+                }
+
+                return service.checkin({
+                    barcode : params.copy_barcode,
+                    noop : true,
+                    void_overdues : args.forgive_fines
+                }).then(function(checkin_resp) {
+                    if (checkin_resp.evt[0].textcode == 'SUCCESS') {
                         return service.checkout(params, options);
                     } else {
-                        alert(egCore.evt.parse(evt));
+                        alert(egCore.evt.parse(checkin_resp.evt[0]));
                         return $q.reject();
                     }
                 });
@@ -844,11 +1027,12 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     }
 
     service.backdate_dialog = function(circ_ids) {
-        return $modal.open({
+        return $uibModal.open({
             templateUrl: './circ/share/t_backdate_dialog',
+            backdrop: 'static',
             controller: 
-                       ['$scope','$modalInstance',
-                function($scope , $modalInstance) {
+                       ['$scope','$uibModalInstance',
+                function($scope , $uibModalInstance) {
 
                 var today = new Date();
                 $scope.dialog = {
@@ -864,7 +1048,7 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
 
 
                 $scope.cancel = function() { 
-                    $modalInstance.dismiss();
+                    $uibModalInstance.dismiss();
                 }
 
                 $scope.ok = function() { 
@@ -873,7 +1057,7 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
                     service.batch_backdate(circ_ids, bd)
                     .then(
                         function() { // on complete
-                            $modalInstance.close({backdate : bd});
+                            $uibModalInstance.close({backdate : bd});
                         },
                         null,
                         function(resp) { // on response
@@ -933,11 +1117,12 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     service.mark_claims_returned_dialog = function(copy_barcodes) {
         if (!copy_barcodes.length) return;
 
-        return $modal.open({
+        return $uibModal.open({
             templateUrl: './circ/share/t_mark_claims_returned_dialog',
+            backdrop: 'static',
             controller: 
-                       ['$scope','$modalInstance',
-                function($scope , $modalInstance) {
+                       ['$scope','$uibModalInstance',
+                function($scope , $uibModalInstance) {
 
                 var today = new Date();
                 $scope.args = {
@@ -950,7 +1135,7 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
                         $scope.args.backdate = today;
                 });
 
-                $scope.cancel = function() {$modalInstance.dismiss()}
+                $scope.cancel = function() {$uibModalInstance.dismiss()}
                 $scope.ok = function() { 
 
                     var date = $scope.args.date.toISOString().replace(/T.*/,'');
@@ -963,7 +1148,7 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
                         var bc = copy_barcodes.pop();
                         if (!bc) {
                             deferred.resolve();
-                            $modalInstance.close();
+                            $uibModalInstance.close();
                             return;
                         }
 
@@ -1013,32 +1198,80 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         return deferred.promise;
     }
 
-    service.mark_damaged = function(copy_ids) {
-        return egConfirmDialog.open(
-            egCore.strings.MARK_DAMAGED_CONFIRM, '',
-            {   num_items : copy_ids.length,
-                ok : function() {},
-                cancel : function() {}
-            }
+    service.mark_damaged = function(params) {
+        if (!params) return $q.when();
+        return $uibModal.open({
+            templateUrl: './circ/share/t_mark_damaged',
+            controller:
+                ['$scope', '$uibModalInstance', 'egCore', 'egBilling', 'egItem',
+                function($scope, $uibModalInstance, egCore, egBilling, egItem) {
+                    var doRefresh = params.refresh;
+                    
+                    $scope.billArgs = {charge: params.charge};
+                    $scope.mode = 'charge';
+                    $scope.barcode = params.barcode;
+                    if (params.charge && params.charge > 0) {
+                        $scope.applyFine = "apply";
+                    }
+                    if (params.circ) {
+                        $scope.circ = params.circ;
+                        $scope.circ_checkin_time = params.circ.checkin_time();
+                        $scope.circ_patron_name = params.circ.usr().family_name() + ", "
+                            + params.circ.usr().first_given_name() + " "
+                            + params.circ.usr().second_given_name();
+                    }
+                    egBilling.fetchBillingTypes().then(function(res) {
+                        $scope.billingTypes = res;
+                    });
 
-        ).result.then(function() {
-            var promises = [];
-            angular.forEach(copy_ids, function(copy_id) {
-                promises.push(
-                    egCore.net.request(
-                        'open-ils.circ',
-                        'open-ils.circ.mark_item_damaged',
-                        egCore.auth.token(), copy_id
-                    ).then(function(resp) {
-                        if (evt = egCore.evt.parse(resp)) {
-                            console.error('mark damaged failed: ' + evt);
-                        }
-                    })
-                );
-            });
+                    $scope.btnChargeFees = function() {
+                        $scope.mode = 'charge';
+                        $scope.billArgs.charge = params.charge;
+                        $scope.applyFine = "apply";
+                    }
+                    $scope.btnWaiveFees = function() {
+                        $scope.mode = 'waive';
+                        $scope.billArgs.charge = 0;
+                        $scope.applyFine = "noapply";
+                    }
 
-            return $q.all(promises);
-        });
+                    $scope.cancel = function ($event) { 
+                        $uibModalInstance.dismiss();
+                    }
+                    $scope.ok = function() {
+                        handle_mark_item_damaged();
+                    }
+
+                    var handle_mark_item_damaged = function() {
+                        egCore.net.request(
+                            'open-ils.circ',
+                            'open-ils.circ.mark_item_damaged',
+                            egCore.auth.token(), params.id, {
+                                apply_fines: $scope.applyFine,
+                                override_amount: $scope.billArgs.charge,
+                                override_btype: $scope.billArgs.type,
+                                override_note: $scope.billArgs.note,
+                                handle_checkin: !$scope.applyFine
+                        }).then(function(resp) {
+                            if (evt = egCore.evt.parse(resp)) {
+                                doRefresh = false;
+                                console.debug("mark damaged more information required. Pushing back.");
+                                service.mark_damaged({
+                                    id: params.id,
+                                    barcode: params.barcode,
+                                    charge: evt.payload.charge,
+                                    circ: evt.payload.circ,
+                                    refresh: params.refresh
+                                });
+                                console.error('mark damaged failed: ' + evt);
+                            }
+                        }).then(function() {
+                            if (doRefresh) egItem.add_barcode_to_list(params.barcode);
+                        });
+                        $uibModalInstance.close();
+                    }
+                }]
+        }).result;
     }
 
     service.mark_missing = function(copy_ids) {
@@ -1126,11 +1359,26 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         });
     }
 
+    service.add_copy_alerts = function(item_ids) {
+        return egAddCopyAlertDialog.open({
+            copy_ids : item_ids,
+            ok : function() { },
+            cancel : function() {}
+        }).result.then(function() { });
+    }
 
+    service.manage_copy_alerts = function(item_ids) {
+        return egCopyAlertEditorDialog.open({
+            copy_id : item_ids[0],
+            ok : function() { },
+            cancel : function() {}
+        }).result.then(function() { });
+    }
 
     // alert when copy location alert_message is set.
     // This does not affect processing, it only produces a click-through
     service.handle_checkin_loc_alert = function(evt, params, options) {
+        if (angular.isArray(evt)) evt = evt[0];
 
         var copy = evt && evt.payload ? evt.payload.copy : null;
 
@@ -1145,25 +1393,26 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     }
 
     service.handle_checkin_resp = function(evt, params, options) {
+        if (!angular.isArray(evt)) evt = [evt];
 
         var final_resp = {evt : evt, params : params, options : options};
 
         var copy, hold, transit;
-        if (evt.payload) {
-            copy = evt.payload.copy;
-            hold = evt.payload.hold;
-            transit = evt.payload.transit;
+        if (evt[0].payload) {
+            copy = evt[0].payload.copy;
+            hold = evt[0].payload.hold;
+            transit = evt[0].payload.transit;
         }
 
         // track the barcode regardless of whether it's valid
-        evt.copy_barcode = params.copy_barcode;
+        angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
 
-        console.debug('checkin event ' + evt.textcode);
+        angular.forEach(evt, function(e){ console.debug('checkin event ' + e.textcode); });
 
-        if (service.checkin_overridable_events.indexOf(evt.textcode) > -1) 
+        if (evt.filter(function(e){return service.checkin_overridable_events.indexOf(e.textcode) > -1;}).length > 0)
             return service.handle_overridable_checkin_event(evt, params, options);
 
-        switch (evt.textcode) {
+        switch (evt[0].textcode) {
 
             case 'SUCCESS':
             case 'NO_CHANGE':
@@ -1174,28 +1423,31 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
                     case 4: /* MISSING */                                          
                     case 7: /* RESHELVING */ 
 
+                        egCore.audio.play('success.checkin');
+
                         // see if the copy location requires an alert
                         return service.handle_checkin_loc_alert(evt, params, options)
                         .then(function() {return final_resp});
 
                     case 8: /* ON HOLDS SHELF */
-
+                        egCore.audio.play('info.checkin.holds_shelf');
                         
                         if (hold) {
 
                             if (hold.pickup_lib() == egCore.auth.user().ws_ou()) {
                                 // inform user if the item is on the local holds shelf
                             
-                                evt.route_to = egCore.strings.ROUTE_TO_HOLDS_SHELF;
+                                evt[0].route_to = egCore.strings.ROUTE_TO_HOLDS_SHELF;
                                 return service.route_dialog(
                                     './circ/share/t_hold_shelf_dialog', 
-                                    evt, params, options
+                                    evt[0], params, options
                                 ).then(function() { return final_resp });
 
                             } else {
                                 // normally, if the hold was on the shelf at a 
                                 // different location, it would be put into 
                                 // transit, resulting in a ROUTE_ITEM event.
+                                egCore.audio.play('warning.checkin.wrong_shelf');
                                 return $q.when(final_resp);
                             }
                         } else {
@@ -1206,14 +1458,17 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
                         }
 
                     case 11: /* CATALOGING */
-                        evt.route_to = egCore.strings.ROUTE_TO_CATALOGING;
+                        egCore.audio.play('info.checkin.cataloging');
+                        evt[0].route_to = egCore.strings.ROUTE_TO_CATALOGING;
                         return $q.when(final_resp);
 
                     case 15: /* ON_RESERVATION_SHELF */
+                        egCore.audio.play('info.checkin.reservation');
                         // TODO: show booking reservation dialog
                         return $q.when(final_resp);
 
                     default:
+                        egCore.audio.play('error.checkin.unknown');
                         console.error('Unhandled checkin copy status: ' 
                             + copy.status().id() + ' : ' + copy.status().name());
                         return $q.when(final_resp);
@@ -1222,16 +1477,18 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
             case 'ROUTE_ITEM':
                 return service.route_dialog(
                     './circ/share/t_transit_dialog', 
-                    evt, params, options
+                    evt[0], params, options
                 ).then(function() { return final_resp });
 
             case 'ASSET_COPY_NOT_FOUND':
+                egCore.audio.play('error.checkin.not_found');
                 return egAlertDialog.open(
                     egCore.strings.UNCAT_ALERT_DIALOG, params)
                     .result.then(function() {return final_resp});
 
             case 'ITEM_NOT_CATALOGED':
-                evt.route_to = egCore.strings.ROUTE_TO_CATALOGING;
+                egCore.audio.play('error.checkin.not_cataloged');
+                evt[0].route_to = egCore.strings.ROUTE_TO_CATALOGING;
                 if (options.no_precat_alert) 
                     return $q.when(final_resp);
                 return egAlertDialog.open(
@@ -1239,7 +1496,8 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
                     .result.then(function() {return final_resp});
 
             default:
-                console.warn('unhandled checkin response : ' + evt.textcode);
+                egCore.audio.play('error.checkin.unknown');
+                console.warn('unhandled checkin response : ' + evt[0].textcode);
                 return $q.when(final_resp);
         }
     }
@@ -1247,6 +1505,7 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     // collect transit, address, and hold info that's not already
     // included in responses.
     service.collect_route_data = function(tmpl, evt, params, options) {
+        if (angular.isArray(evt)) evt = evt[0];
         var promises = [];
         var data = {};
 
@@ -1268,10 +1527,20 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
             );
         }
 
+
         if (!tmpl.match(/hold_shelf/)) {
+            var courier_deferred = $q.defer();
+            promises.push(courier_deferred.promise);
             promises.push(
                 service.find_copy_transit(evt, params, options)
-                .then(function(trans) {data.transit = trans})
+                .then(function(trans) {
+                    data.transit = trans;
+                    egCore.org.settings('lib.courier_code', trans.dest().id())
+                    .then(function(s) {
+                        data.dest_courier_code = s['lib.courier_code'];
+                        courier_deferred.resolve();
+                    });
+                })
             );
         }
 
@@ -1279,37 +1548,63 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     }
 
     service.route_dialog = function(tmpl, evt, params, options) {
+        if (angular.isArray(evt)) evt = evt[0];
 
         return service.collect_route_data(tmpl, evt, params, options)
         .then(function(data) {
-            
+
+            var template = data.transit ?
+                (data.patron ? 'hold_transit_slip' : 'transit_slip') :
+                'hold_shelf_slip';
+            if (service.never_auto_print[template]) {
+                // do not show the dialog or print if the
+                // disabled automatic print attempt type list includes
+                // the specified template
+                return;
+            }
+
             // All actions flow from the print data
 
             var print_context = {
                 copy : egCore.idl.toHash(evt.payload.copy),
                 title : evt.title,
-                author : evt.author
-            }
+                author : evt.author,
+                call_number : egCore.idl.toHash(evt.payload.volume)
+            };
+
+            var acn = print_context.call_number; // fix up pre/suffixes
+            if (acn.prefix == -1) acn.prefix = "";
+            if (acn.suffix == -1) acn.suffix = "";
 
             if (data.transit) {
                 // route_dialog includes the "route to holds shelf" 
                 // dialog, which has no transit
                 print_context.transit = egCore.idl.toHash(data.transit);
-                print_context.dest_address = egCore.idl.toHash(data.address);
+                print_context.dest_courier_code = data.dest_courier_code;
+                if (data.address) {
+                    print_context.dest_address = egCore.idl.toHash(data.address);
+                }
                 print_context.dest_location =
                     egCore.idl.toHash(egCore.org.get(data.transit.dest()));
             }
 
             if (data.patron) {
                 print_context.hold = egCore.idl.toHash(evt.payload.hold);
+                var notes = print_context.hold.notes;
+                if(notes.length > 0){
+                    print_context.hold_notes = [];
+                    angular.forEach(notes, function(n){
+                        print_context.hold_notes.push(n);
+                    });
+                }
                 print_context.patron = egCore.idl.toHash(data.patron);
             }
 
-            function print_transit() {
-                var template = data.transit ? 
-                    (data.patron ? 'hold_transit_slip' : 'transit_slip') :
-                    'hold_shelf_slip';
+            var sound = 'info.checkin.transit';
+            if (evt.payload.hold) sound += '.hold';
+            egCore.audio.play(sound);
 
+            function print_transit(template) {
                 return egCore.print.print({
                     context : 'default', 
                     template : template, 
@@ -1320,13 +1615,14 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
             // when auto-print is on, skip the dialog and go straight
             // to printing.
             if (options.auto_print_holds_transits) 
-                return print_transit();
+                return print_transit(template);
 
-            return $modal.open({
+            return $uibModal.open({
                 templateUrl: tmpl,
+                backdrop: 'static',
                 controller: [
-                            '$scope','$modalInstance',
-                    function($scope , $modalInstance) {
+                            '$scope','$uibModalInstance',
+                    function($scope , $uibModalInstance) {
 
                     $scope.today = new Date();
 
@@ -1335,11 +1631,11 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
                         $scope[key] = val;
                     });
 
-                    $scope.ok = function() {$modalInstance.close()}
+                    $scope.ok = function() {$uibModalInstance.close()}
 
                     $scope.print = function() { 
-                        $modalInstance.close();
-                        print_transit();
+                        $uibModalInstance.close();
+                        print_transit(template);
                     }
                 }]
 
@@ -1349,17 +1645,66 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
 
     // action == what action to take if the user confirms the alert
     service.copy_alert_dialog = function(evt, params, options, action) {
-        return egConfirmDialog.open(
-            egCore.strings.COPY_ALERT_MSG_DIALOG_TITLE, 
-            evt.payload,  // payload == alert message text
-            {   copy_barcode : params.copy_barcode,
-                ok : function() {},
+        if (angular.isArray(evt)) evt = evt[0];
+        if (!angular.isArray(evt.payload)) {
+            return egConfirmDialog.open(
+                egCore.strings.COPY_ALERT_MSG_DIALOG_TITLE, 
+                evt.payload,  // payload == alert message text
+                {   copy_barcode : params.copy_barcode,
+                    ok : function() {},
+                    cancel : function() {}
+                }
+            ).result.then(function() {
+                options.override = true;
+                return service[action](params, options);
+            });
+        } else { // we got a list of copy alert objects ...
+            return egCopyAlertManagerDialog.open({
+                alerts : evt.payload,
+                mode : action,
+                ok : function(the_next_status) {
+                        if (the_next_status !== null) {
+                            params.next_copy_status = [ the_next_status ];
+                        }
+                     },
                 cancel : function() {}
+            }).result.then(function() {
+                options.override = true;
+                return service[action](params, options);
+            });
+        }
+    }
+
+    // action == what action to take if the user confirms the alert
+    service.hold_capture_delay_dialog = function(evt, params, options, action) {
+        if (angular.isArray(evt)) evt = evt[0];
+        return $uibModal.open({
+            templateUrl: './circ/checkin/t_hold_verify',
+            backdrop: 'static',
+            controller:
+                       ['$scope','$uibModalInstance','params',
+                function($scope , $uibModalInstance , params) {
+                $scope.copy_barcode = params.copy_barcode;
+                $scope.capture = function() {
+                    params.capture = 'capture';
+                    $uibModalInstance.close();
+                };
+                $scope.nocapture = function() {
+                    params.capture = 'nocapture';
+                    $uibModalInstance.close();
+                };
+                $scope.cancel = function() { $uibModalInstance.dismiss(); };
+            }],
+            resolve : {
+                params : function() {
+                    return params;
+                }
             }
-        ).result.then(function() {
-            options.override = true;
-            return service[action](params, options);
-        });
+        }).result.then(
+            function(r) {
+                return service[action](params, options);
+            }
+        );
     }
 
     // check the barcode.  If it's no good, show the warning dialog
@@ -1369,14 +1714,16 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         var ok = service.check_barcode(bc);
         if (ok) return $q.when();
 
-        return $modal.open({
+        egCore.audio.play('warning.circ.bad_barcode');
+        return $uibModal.open({
             templateUrl: './circ/share/t_bad_barcode_dialog',
+            backdrop: 'static',
             controller: 
-                ['$scope', '$modalInstance', 
-                function($scope, $modalInstance) {
+                ['$scope', '$uibModalInstance', 
+                function($scope, $uibModalInstance) {
                 $scope.barcode = bc;
-                $scope.ok = function() { $modalInstance.close() }
-                $scope.cancel = function() { $modalInstance.dismiss() }
+                $scope.ok = function() { $uibModalInstance.close() }
+                $scope.cancel = function() { $uibModalInstance.dismiss() }
             }]
         }).result;
     }
@@ -1415,21 +1762,90 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         return check_digit;
     }
 
+    service.handle_barcode_completion = function(barcode) {
+        return egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.get_barcodes',
+            egCore.auth.token(), egCore.auth.user().ws_ou(), 
+            'asset', barcode)
+
+        .then(function(resp) {
+            // TODO: handle event during barcode lookup
+            if (evt = egCore.evt.parse(resp)) {
+                console.error(evt.toString());
+                return $q.reject();
+            }
+
+            // no matching barcodes: return the barcode as entered
+            // by the user (so that, e.g., checkout can fall back to
+            // precat/noncat handling)
+            if (!resp || !resp[0]) {
+                return barcode;
+            }
+
+            // exactly one matching barcode: return it
+            if (resp.length == 1) {
+                return resp[0].barcode;
+            }
+
+            // multiple matching barcodes: let the user pick one 
+            console.debug('multiple matching barcodes');
+            var matches = [];
+            var promises = [];
+            var final_barcode;
+            angular.forEach(resp, function(cp) {
+                promises.push(
+                    egCore.net.request(
+                        'open-ils.circ',
+                        'open-ils.circ.copy_details.retrieve',
+                        egCore.auth.token(), cp.id
+                    ).then(function(r) {
+                        matches.push({
+                            barcode: r.copy.barcode(),
+                            title: r.mvr.title(),
+                            org_name: egCore.org.get(r.copy.circ_lib()).name(),
+                            org_shortname: egCore.org.get(r.copy.circ_lib()).shortname()
+                        });
+                    })
+                );
+            });
+            return $q.all(promises)
+            .then(function() {
+                return $uibModal.open({
+                    templateUrl: './circ/share/t_barcode_choice_dialog',
+                    backdrop: 'static',
+                    controller:
+                        ['$scope', '$uibModalInstance',
+                        function($scope, $uibModalInstance) {
+                        $scope.matches = matches;
+                        $scope.ok = function(barcode) {
+                            $uibModalInstance.close();
+                            final_barcode = barcode;
+                        }
+                        $scope.cancel = function() {$uibModalInstance.dismiss()}
+                    }],
+                }).result.then(function() { return final_barcode });
+            })
+        });
+    }
+
     service.create_penalty = function(user_id) {
-        return $modal.open({
+        return $uibModal.open({
             templateUrl: './circ/share/t_new_message_dialog',
+            backdrop: 'static',
             controller: 
-                   ['$scope','$modalInstance','staffPenalties',
-            function($scope , $modalInstance , staffPenalties) {
+                   ['$scope','$uibModalInstance','staffPenalties',
+            function($scope , $uibModalInstance , staffPenalties) {
                 $scope.focusNote = true;
                 $scope.penalties = staffPenalties;
+                $scope.require_initials = service.require_initials;
                 $scope.args = {penalty : 21}; // default to Note
                 $scope.setPenalty = function(id) {
                     args.penalty = id;
                 }
-                $scope.ok = function(count) { $modalInstance.close($scope.args) }
+                $scope.ok = function(count) { $uibModalInstance.close($scope.args) }
                 $scope.cancel = function($event) { 
-                    $modalInstance.dismiss();
+                    $uibModalInstance.dismiss();
                     $event.preventDefault();
                 }
             }],
@@ -1440,7 +1856,12 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
                 pen.usr(user_id);
                 pen.org_unit(egCore.auth.user().ws_ou());
                 pen.note(args.note);
-                pen.standing_penalty(args.penalty);
+                if (args.initials) pen.note(args.note + ' [' + args.initials + ']');
+                if (args.custom_penalty) {
+                    pen.standing_penalty(args.custom_penalty);
+                } else {
+                    pen.standing_penalty(args.penalty);
+                }
                 pen.staff(egCore.auth.user().id());
                 pen.set_date('now');
                 return egCore.pcrud.create(pen);
@@ -1450,21 +1871,23 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
 
     // assumes, for now anyway,  penalty type is fleshed onto usr_penalty.
     service.edit_penalty = function(usr_penalty) {
-        return $modal.open({
+        return $uibModal.open({
             templateUrl: './circ/share/t_new_message_dialog',
+            backdrop: 'static',
             controller: 
-                   ['$scope','$modalInstance','staffPenalties',
-            function($scope , $modalInstance , staffPenalties) {
+                   ['$scope','$uibModalInstance','staffPenalties',
+            function($scope , $uibModalInstance , staffPenalties) {
                 $scope.focusNote = true;
                 $scope.penalties = staffPenalties;
+                $scope.require_initials = service.require_initials;
                 $scope.args = {
                     penalty : usr_penalty.standing_penalty().id(),
                     note : usr_penalty.note()
                 }
                 $scope.setPenalty = function(id) { args.penalty = id; }
-                $scope.ok = function(count) { $modalInstance.close($scope.args) }
+                $scope.ok = function(count) { $uibModalInstance.close($scope.args) }
                 $scope.cancel = function($event) { 
-                    $modalInstance.dismiss();
+                    $uibModalInstance.dismiss();
                     $event.preventDefault();
                 }
             }],
@@ -1472,6 +1895,7 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         }).result.then(
             function(args) {
                 usr_penalty.note(args.note);
+                if (args.initials) usr_penalty.note(args.note + ' [' + args.initials + ']');
                 usr_penalty.standing_penalty(args.penalty);
                 return egCore.pcrud.update(usr_penalty);
             }