LP#1570091: webstaff: adding item status actions
authorMike Rylander <mrylander@gmail.com>
Thu, 3 Mar 2016 17:13:08 +0000 (12:13 -0500)
committerBill Erickson <berickxx@gmail.com>
Wed, 20 Apr 2016 14:51:32 +0000 (10:51 -0400)
  - add items to bucket
  - request items
  - Link as Conjoined to Previously Marked Bib Record
  - Delete Copies
  - Check In Copies
  - Renew Copies
  - Show Triggered Events
  - Show Item Holds
  - Show Record Holds
  - Mark Item As Damaged
  - Mark Item as Missing
  - Add Copies
  - Add Volumes and Copies
  - Edit Volumes
  - Edit Copies
  - Edit Volumes and Copies
  - Replace Barcodes
  - Transfer Items to Previously Marked Library
  - Transfer Items to Previously Marked Volume

Signed-off-by: Mike Rylander <mrylander@gmail.com>
Signed-off-by: Galen Charlton <gmc@esilibrary.com>
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/templates/staff/cat/item/index.tt2
Open-ILS/src/templates/staff/cat/item/t_list.tt2
Open-ILS/web/js/ui/default/staff/cat/item/app.js

index 2232a7d..a4f297c 100644 (file)
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/item/app.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/services/record.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/services/billing.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/services/circ.js"></script>
+[% INCLUDE 'staff/circ/share/circ_strings.tt2' %]
+<script>
+  angular.module('egCoreMod').run(['egStrings', function(s) {
+    s.CONFIRM_DELETE_COPIES_VOLUMES =
+      "[% l('Permanently delete selected copies and/or volumes from catalog?') %]";
+    s.CONFIRM_DELETE_COPIES_VOLUMES_MESSAGE =
+      "[% l('Will delete {{copies}} copies and {{volumes}} volumes') %]";
+    s.CONFIRM_DELETE_PEERS =
+      "[% l('Unlink selected conjoined copies?') %]";
+    s.CONFIRM_DELETE_PEERS_MESSAGE =
+      "[% l('Will unlink {{peers}} copies') %]";
+    s.CONFIRM_TRANSFER_COPIES_TO_MARKED_VOLUME =
+      "[% l('Are you sure you want to transfer selected items to the marked volume?') %]";
+    s.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE =
+      "[% l('One or more items could not be transferred. Override?') %]";
+    s.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY =
+      "[% l('Reason(s) include: [_1]', '{{evt_desc}}') %]";
+  }])
+</script>
 [% END %]
 
 <style>
index f58ea81..6dddad3 100644 (file)
@@ -7,6 +7,50 @@
   grid-controls="gridControls"
   persist-key="cat.items">
 
+  <eg-grid-action handler="add_copies_to_bucket"
+    label="[% l('Add Items to Bucket') %]"></eg-grid-action>
+  <eg-grid-action handler="requestItems"
+    label="[% l('Request Items') %]"></eg-grid-action>
+  <eg-grid-action handler="attach_to_peer_bib"
+    label="[% l('Link as Conjoined to Previously Marked Bib Record') %]"></eg-grid-action>
+  <eg-grid-action handler="selectedHoldingsCopyDelete"
+    label="[% l('Delete Copies') %]"></eg-grid-action>
+  <eg-grid-action handler="checkin"
+    label="[% l('Check In Copies') %]"></eg-grid-action>
+  <eg-grid-action handler="renew"
+    label="[% l('Renew Copies') %]"></eg-grid-action>
+    
+  <eg-grid-action handler="selectedHoldingsItemStatusTgrEvt" group="[% l('Show') %]"
+    label="[% l('Triggered Events') %]"></eg-grid-action>
+  <eg-grid-action handler="selectedHoldingsItemStatusHolds" group="[% l('Show') %]"
+    label="[% l('Item Holds') %]"></eg-grid-action>
+  <eg-grid-action handler="showBibHolds" group="[% l('Show') %]"
+    label="[% l('Record Holds') %]"></eg-grid-action>
+    
+  <eg-grid-action handler="selectedHoldingsDamaged" group="[% l('Mark') %]"
+    label="[% l('Item as Damaged') %]"></eg-grid-action>
+  <eg-grid-action handler="selectedHoldingsMissing" group="[% l('Mark') %]"
+    label="[% l('Item as Missing') %]"></eg-grid-action>
+    
+  <eg-grid-action handler="selectedHoldingsCopyAdd" group="[% l('Add') %]"
+    label="[% l('Copies') %]"></eg-grid-action>
+  <eg-grid-action handler="selectedHoldingsVolCopyAdd" group="[% l('Add') %]"
+    label="[% l('Volumes and Copies') %]"></eg-grid-action>
+
+  <eg-grid-action handler="selectedHoldingsVolEdit" group="[% l('Edit') %]"
+    label="[% l('Volumes') %]"></eg-grid-action>
+  <eg-grid-action handler="selectedHoldingsCopyEdit" group="[% l('Edit') %]"
+    label="[% l('Copies') %]"></eg-grid-action>
+  <eg-grid-action handler="selectedHoldingsVolCopyEdit" group="[% l('Edit') %]"
+    label="[% l('Volumes and Copies') %]"></eg-grid-action>
+  <eg-grid-action handler="replaceBarcodes" group="[% l('Edit') %]"
+    label="[% l('Replace Barcodes') %]"></eg-grid-action>
+
+  <eg-grid-action handler="changeItemOwningLib" group="[% l('Transfer') %]"
+    label="[% l('Items to Previously Marked Library') %]"></eg-grid-action>
+  <eg-grid-action handler="transferItems" group="[% l('Transfer') %]"
+    label="[% l('Items to Previously Marked Volume') %]"></eg-grid-action>
+
   <eg-grid-field label="[% l('Barcode') %]"     path='barcode' visible></eg-grid-field>
   <eg-grid-field label="[% l('Call Number') %]" path="call_number.label" visible></eg-grid-field>
   <eg-grid-field label="[% l('Location') %]"    path="location.name" visible></eg-grid-field>
index 837b5b3..a2750ce 100644 (file)
@@ -3,7 +3,7 @@
  */
 
 angular.module('egItemStatus', 
-    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod'])
 
 .filter('boolText', function(){
     return function (v) {
@@ -139,8 +139,8 @@ function($scope , $location , egCore , egGridDataProvider , itemSvc) {
  * List view - grid stuff
  */
 .controller('ListCtrl', 
-       ['$scope','$q','$routeParams','$location','$timeout','egCore','egGridDataProvider','itemSvc',
-function($scope , $q , $routeParams , $location , $timeout , egCore , egGridDataProvider , itemSvc) {
+       ['$scope','$q','$routeParams','$location','$timeout','$window','egCore','egGridDataProvider','itemSvc','egUser','$modal','egCirc','egConfirmDialog',
+function($scope , $q , $routeParams , $location , $timeout , $window , egCore , egGridDataProvider , itemSvc , egUser , $modal , egCirc , egConfirmDialog) {
     var copyId = [];
     var cp_list = $routeParams.idList;
     if (cp_list) {
@@ -211,6 +211,10 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egGridData
         })
     }
 
+    var add_barcode_to_list = function (b) {
+        $scope.context.search({barcode:b});
+    }
+
     $scope.context.toggleDisplay = function() {
         var item = copyGrid.selectedItems()[0];
         if (item) 
@@ -223,6 +227,566 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egGridData
             $location.path('/cat/item/' + item.id + '/triggered_events');
     }
 
+    function gatherSelectedRecordIds () {
+        var rid_list = [];
+        angular.forEach(
+            copyGrid.selectedItems(),
+            function (item) {
+                if (rid_list.indexOf(item['call_number.record.id']) == -1)
+                    rid_list.push(item['call_number.record.id'])
+            }
+        );
+        return rid_list;
+    }
+
+    function gatherSelectedVolumeIds (rid) {
+        var cn_id_list = [];
+        angular.forEach(
+            copyGrid.selectedItems(),
+            function (item) {
+                if (rid && item['call_number.record.id'] != rid) return;
+                if (cn_id_list.indexOf(item['call_number.id']) == -1)
+                    cn_id_list.push(item['call_number.id'])
+            }
+        );
+        return cn_id_list;
+    }
+
+    function gatherSelectedHoldingsIds (rid) {
+        var cp_id_list = [];
+        angular.forEach(
+            copyGrid.selectedItems(),
+            function (item) {
+                if (rid && item['call_number.record.id'] != rid) return;
+                cp_id_list.push(item.id)
+            }
+        );
+        return cp_id_list;
+    }
+
+    $scope.add_copies_to_bucket = function() {
+        var copy_list = gatherSelectedHoldingsIds();
+        if (copy_list.length == 0) return;
+
+        return $modal.open({
+            templateUrl: './cat/catalog/t_add_to_bucket',
+            animation: true,
+            size: 'md',
+            controller:
+                   ['$scope','$modalInstance',
+            function($scope , $modalInstance) {
+
+                $scope.bucket_id = 0;
+                $scope.newBucketName = '';
+                $scope.allBuckets = [];
+
+                egCore.net.request(
+                    'open-ils.actor',
+                    'open-ils.actor.container.retrieve_by_class.authoritative',
+                    egCore.auth.token(), egCore.auth.user().id(),
+                    'copy', 'staff_client'
+                ).then(function(buckets) { $scope.allBuckets = buckets; });
+
+                $scope.add_to_bucket = function() {
+                    var promises = [];
+                    angular.forEach(copy_list, function (cp) {
+                        var item = new egCore.idl.ccbi()
+                        item.bucket($scope.bucket_id);
+                        item.target_copy(cp);
+                        promises.push(
+                            egCore.net.request(
+                                'open-ils.actor',
+                                'open-ils.actor.container.item.create',
+                                egCore.auth.token(), 'copy', item
+                            )
+                        );
+
+                        return $q.all(promises).then(function() {
+                            $modalInstance.close();
+                        });
+                    });
+                }
+
+                $scope.add_to_new_bucket = function() {
+                    var bucket = new egCore.idl.ccb();
+                    bucket.owner(egCore.auth.user().id());
+                    bucket.name($scope.newBucketName);
+                    bucket.description('');
+                    bucket.btype('staff_client');
+
+                    return egCore.net.request(
+                        'open-ils.actor',
+                        'open-ils.actor.container.create',
+                        egCore.auth.token(), 'copy', bucket
+                    ).then(function(bucket) {
+                        $scope.bucket_id = bucket;
+                        $scope.add_to_bucket();
+                    });
+                }
+
+                $scope.cancel = function() {
+                    $modalInstance.dismiss();
+                }
+            }]
+        });
+    }
+
+    $scope.requestItems = function() {
+        var copy_list = gatherSelectedHoldingsIds();
+        if (copy_list.length == 0) return;
+
+        return $modal.open({
+            templateUrl: './cat/catalog/t_request_items',
+            animation: true,
+            controller:
+                   ['$scope','$modalInstance','egUser',
+            function($scope , $modalInstance , egUser) {
+                $scope.user = null;
+                $scope.first_user_fetch = true;
+
+                $scope.hold_data = {
+                    hold_type : 'C',
+                    copy_list : copy_list,
+                    pickup_lib: egCore.org.get(egCore.auth.user().ws_ou()),
+                    user      : egCore.auth.user().id()
+                };
+
+                egUser.get( $scope.hold_data.user ).then(function(u) {
+                    $scope.user = u;
+                    $scope.barcode = u.card().barcode();
+                    $scope.user_name = egUser.format_name(u);
+                    $scope.hold_data.user = u.id();
+                });
+
+                $scope.user_name = '';
+                $scope.barcode = '';
+                $scope.$watch('barcode', function (n) {
+                    if (!$scope.first_user_fetch) {
+                        egUser.getByBarcode(n).then(function(u) {
+                            $scope.user = u;
+                            $scope.user_name = egUser.format_name(u);
+                            $scope.hold_data.user = u.id();
+                        }, function() {
+                            $scope.user = null;
+                            $scope.user_name = '';
+                            delete $scope.hold_data.user;
+                        });
+                    }
+                    $scope.first_user_fetch = false;
+                });
+
+                $scope.ok = function(h) {
+                    var args = {
+                        patronid  : h.user,
+                        hold_type : h.hold_type,
+                        pickup_lib: h.pickup_lib.id(),
+                        depth     : 0
+                    };
+
+                    egCore.net.request(
+                        'open-ils.circ',
+                        'open-ils.circ.holds.test_and_create.batch.override',
+                        egCore.auth.token(), args, h.copy_list
+                    );
+
+                    $modalInstance.close();
+                }
+
+                $scope.cancel = function($event) {
+                    $modalInstance.dismiss();
+                    $event.preventDefault();
+                }
+            }]
+        });
+    }
+
+    $scope.replaceBarcodes = function() {
+        angular.forEach(copyGrid.selectedItems(), function (cp) {
+            $modal.open({
+                templateUrl: './cat/share/t_replace_barcode',
+                animation: true,
+                controller:
+                           ['$scope','$modalInstance',
+                    function($scope , $modalInstance) {
+                        $scope.isModal = true;
+                        $scope.focusBarcode = false;
+                        $scope.focusBarcode2 = true;
+                        $scope.barcode1 = cp.barcode;
+
+                        $scope.updateBarcode = function() {
+                            $scope.copyNotFound = false;
+                            $scope.updateOK = false;
+
+                            egCore.pcrud.search('acp',
+                                {deleted : 'f', barcode : $scope.barcode1})
+                            .then(function(copy) {
+
+                                if (!copy) {
+                                    $scope.focusBarcode = true;
+                                    $scope.copyNotFound = true;
+                                    return;
+                                }
+
+                                $scope.copyId = copy.id();
+                                copy.barcode($scope.barcode2);
+
+                                egCore.pcrud.update(copy).then(function(stat) {
+                                    $scope.updateOK = stat;
+                                    $scope.focusBarcode = true;
+                                    if (stat) add_barcode_to_list(copy.barcode());
+                                });
+
+                            });
+                            $modalInstance.close();
+                        }
+
+                        $scope.cancel = function($event) {
+                            $modalInstance.dismiss();
+                            $event.preventDefault();
+                        }
+                    }
+                ]
+            });
+        });
+    }
+
+    $scope.attach_to_peer_bib = function() {
+        if (copyGrid.selectedItems().length == 0) return;
+
+        egCore.hatch.getItem('eg.cat.marked_conjoined_record').then(function(target_record) {
+            if (!target_record) return;
+
+            return $modal.open({
+                templateUrl: './cat/catalog/t_conjoined_selector',
+                animation: true,
+                controller:
+                       ['$scope','$modalInstance',
+                function($scope , $modalInstance) {
+                    $scope.update = false;
+
+                    $scope.peer_type = null;
+                    $scope.peer_type_list = [];
+
+                    get_peer_types = function() {
+                        if (egCore.env.bpt)
+                            return $q.when(egCore.env.bpt.list);
+
+                        return egCore.pcrud.retrieveAll('bpt', null, {atomic : true})
+                        .then(function(list) {
+                            egCore.env.absorbList(list, 'bpt');
+                            return list;
+                        });
+                    }
+
+                    get_peer_types().then(function(list){
+                        $scope.peer_type_list = list;
+                    });
+
+                    $scope.ok = function(type) {
+                        var promises = [];
+
+                        angular.forEach(copyGrid.selectedItems(), function (cp) {
+                            var n = new egCore.idl.bpbcm();
+                            n.isnew(true);
+                            n.peer_record(target_record);
+                            n.target_copy(cp.id);
+                            n.peer_type(type);
+                            promises.push(egCore.pcrud.create(n).then(function(){add_barcode_to_list(cp.barcode)}));
+                        });
+
+                        return $q.all(promises).then(function(){$modalInstance.close()});
+                    }
+
+                    $scope.cancel = function($event) {
+                        $modalInstance.dismiss();
+                        $event.preventDefault();
+                    }
+                }]
+            });
+        });
+    }
+
+    $scope.selectedHoldingsCopyDelete = function () {
+        var copy_list = gatherSelectedHoldingsIds();
+        if (copy_list.length == 0) return;
+
+        var copy_objects = [];
+        egCore.pcrud.search('acp',
+            {deleted : 'f', id : copy_list},
+            { flesh : 1, flesh_fields : { acp : ['call_number'] } }
+        ).then(function(copy) {
+            copy_objects.push(copy);
+        }).then(function() {
+
+            var cnHash = {};
+            var perCnCopies = {};
+
+            var cn_count = 0;
+            var cp_count = 0;
+
+            angular.forEach(
+                copy_objects,
+                function (cp) {
+                    cp.isdeleted(1);
+                    cp_count++;
+                    var cn_id = cp.call_number().id();
+                    if (!cnHash[cn_id]) {
+                        cnHash[cn_id] = cp.call_number();
+                        perCnCopies[cn_id] = [cp];
+                    } else {
+                        perCnCopies[cn_id].push(cp);
+                    }
+                    cp.call_number(cn_id); // prevent loops in JSON-ification
+                }
+            );
+
+            angular.forEach(perCnCopies, function (v, k) {
+                cnHash[k].copies(v);
+            });
+
+            cnList = [];
+            angular.forEach(cnHash, function (v, k) {
+                cnList.push(v);
+            });
+
+            if (cnList.length == 0) return;
+
+            var flags = {};
+
+            egConfirmDialog.open(
+                egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES,
+                egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES_MESSAGE,
+                {copies : cp_count, volumes : cn_count}
+            ).result.then(function() {
+                egCore.net.request(
+                    'open-ils.cat',
+                    'open-ils.cat.asset.volume.fleshed.batch.update.override',
+                    egCore.auth.token(), cnList, 1, flags
+                ).then(function(){
+                    angular.forEach(copyGrid.selectedItems(), function(cp){add_barcode_to_list(cp.barcode)});
+                });
+            });
+        });
+    }
+
+    $scope.selectedHoldingsItemStatusTgrEvt= function() {
+        var item = copyGrid.selectedItems()[0];
+        if (item)
+            $location.path('/cat/item/' + item.id + '/triggered_events');
+    }
+
+    $scope.selectedHoldingsItemStatusHolds= function() {
+        var item = copyGrid.selectedItems()[0];
+        if (item)
+            $location.path('/cat/item/' + item.id + '/holds');
+    }
+
+    $scope.selectedHoldingsDamaged = function () {
+        var initial_list = copyGrid.selectedItems();
+        egCirc.mark_damaged(gatherSelectedHoldingsIds()).then(function(){
+            angular.forEach(initial_list, function(cp){add_barcode_to_list(cp.barcode)});
+        });
+    }
+
+    $scope.selectedHoldingsMissing = function () {
+        var initial_list = copyGrid.selectedItems();
+        egCirc.mark_missing(gatherSelectedHoldingsIds()).then(function(){
+            angular.forEach(initial_list, function(cp){add_barcode_to_list(cp.barcode)});
+        });
+    }
+
+    $scope.checkin = function () {
+        angular.forEach(copyGrid.selectedItems(), function (cp) {
+            egCirc.checkin({copy_barcode:cp.barcode}).then(
+                function() { add_barcode_to_list(cp.barcode) }
+            );
+        });
+    }
+
+    $scope.renew = function () {
+        angular.forEach(copyGrid.selectedItems(), function (cp) {
+            egCirc.renew({copy_barcode:cp.barcode}).then(
+                function() { add_barcode_to_list(cp.barcode) }
+            );
+        });
+    }
+
+
+    var spawnHoldingsAdd = function (vols,copies){
+        angular.forEach(gatherSelectedRecordIds(), function (r) {
+            var raw = [];
+            if (copies) { // just a copy on existing volumes
+                angular.forEach(gatherSelectedVolumeIds(r), function (v) {
+                    raw.push( {callnumber : v} );
+                });
+            } else if (vols) {
+                angular.forEach(
+                    gatherSelectedHoldingsIds(r),
+                    function (i) {
+                        angular.forEach(copyGrid.selectedItems(), function(item) {
+                            if (i == item.id) raw.push({owner : item['call_number.owning_lib']});
+                        });
+                    }
+                );
+            }
+
+            if (raw.length == 0) raw.push({});
+
+            egCore.net.request(
+                'open-ils.actor',
+                'open-ils.actor.anon_cache.set_value',
+                null, 'edit-these-copies', {
+                    record_id: r,
+                    raw: raw,
+                    hide_vols : false,
+                    hide_copies : false
+                }
+            ).then(function(key) {
+                if (key) {
+                    var url = egCore.env.basePath + 'cat/volcopy/' + key;
+                    $timeout(function() { $window.open(url, '_blank') });
+                } else {
+                    alert('Could not create anonymous cache key!');
+                }
+            });
+        });
+    }
+    $scope.selectedHoldingsVolCopyAdd = function () { spawnHoldingsAdd(true,false) }
+    $scope.selectedHoldingsCopyAdd = function () { spawnHoldingsAdd(false,true) }
+
+    $scope.showBibHolds = function () {
+        angular.forEach(gatherSelectedRecordIds(), function (r) {
+            var url = egCore.env.basePath + 'cat/catalog/record/' + r + '/holds';
+            $timeout(function() { $window.open(url, '_blank') });
+        });
+    }
+
+    var spawnHoldingsEdit = function (hide_vols,hide_copies){
+        angular.forEach(gatherSelectedRecordIds(), function (r) {
+            egCore.net.request(
+                'open-ils.actor',
+                'open-ils.actor.anon_cache.set_value',
+                null, 'edit-these-copies', {
+                    record_id: r,
+                    copies: gatherSelectedHoldingsIds(r),
+                    raw: {},
+                    hide_vols : hide_vols,
+                    hide_copies : hide_copies
+                }
+            ).then(function(key) {
+                if (key) {
+                    var url = egCore.env.basePath + 'cat/volcopy/' + key;
+                    $timeout(function() { $window.open(url, '_blank') });
+                } else {
+                    alert('Could not create anonymous cache key!');
+                }
+            });
+        });
+    }
+    $scope.selectedHoldingsVolCopyEdit = function () { spawnHoldingsEdit(false,false) }
+    $scope.selectedHoldingsVolEdit = function () { spawnHoldingsEdit(false,true) }
+    $scope.selectedHoldingsCopyEdit = function () { spawnHoldingsEdit(true,false) }
+
+    // this "transfers" selected copies to a new owning library,
+    // auto-creating volumes and deleting unused volumes as required.
+    $scope.changeItemOwningLib = function() {
+        var xfer_target = egCore.hatch.getLocalItem('eg.cat.volume_transfer_target');
+        var items = copyGrid.selectedItems();
+        if (!xfer_target || !items.length) {
+            return;
+        }
+        var vols_to_move   = {};
+        var copies_to_move = {};
+        angular.forEach(items, function(item) {
+            if (item['call_number.owning_lib'] != xfer_target) {
+                if (item['call_number.id'] in vols_to_move) {
+                    copies_to_move[item['call_number.id']].push(item.id);
+                } else {
+                    vols_to_move[item['call_number.id']] = {
+                        label       : item['call_number.label'],
+                        label_class : item['call_number.label_class'],
+                        record      : item['call_number.record.id'],
+                        prefix      : item['call_number.prefix.id'],
+                        suffix      : item['call_number.suffix.id']
+                    };
+                    copies_to_move[item['call_number.id']] = new Array;
+                    copies_to_move[item['call_number.id']].push(item.id);
+                }
+            }
+        });
+
+        var promises = [];
+        angular.forEach(vols_to_move, function(vol) {
+            promises.push(egCore.net.request(
+                'open-ils.cat',
+                'open-ils.cat.call_number.find_or_create',
+                egCore.auth.token(),
+                vol.label,
+                vol.record,
+                xfer_target,
+                vol.prefix,
+                vol.suffix,
+                vol.label_class
+            ).then(function(resp) {
+                var evt = egCore.evt.parse(resp);
+                if (evt) return;
+                return egCore.net.request(
+                    'open-ils.cat',
+                    'open-ils.cat.transfer_copies_to_volume',
+                    egCore.auth.token(),
+                    resp.acn_id,
+                    copies_to_move[vol.id]
+                );
+            }));
+        });
+
+        angular.forEach(
+            copyGrid.selectedItems(),
+            function(cp){
+                promises.push(
+                    function(){ add_barcode_to_list(cp.barcode) }
+                )
+            }
+        );
+        $q.all(promises);
+    }
+
+    $scope.transferItems = function (){
+        var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target');
+        var copy_ids = gatherSelectedHoldingsIds();
+        if (xfer_target && copy_ids.length > 0) {
+            egCore.net.request(
+                'open-ils.cat',
+                'open-ils.cat.transfer_copies_to_volume',
+                egCore.auth.token(),
+                xfer_target,
+                copy_ids
+            ).then(
+                function(resp) { // oncomplete
+                    var evt = egCore.evt.parse(resp);
+                    egConfirmDialog.open(
+                        egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE,
+                        egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY,
+                        {'evt_desc': evt.desc}
+                    ).result.then(function() {
+                        egCore.net.request(
+                            'open-ils.cat',
+                            'open-ils.cat.transfer_copies_to_volume.override',
+                            egCore.auth.token(),
+                            xfer_target,
+                            copy_ids,
+                            { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
+                        );
+                    });
+                },
+                null, // onerror
+                null // onprogress
+            ).then(function() {
+                    angular.forEach(copyGrid.selectedItems(), function(cp){add_barcode_to_list(cp.barcode)});
+            });
+        }
+    }
+
     if (copyId.length > 0) {
         itemSvc.fetch(null,copyId).then(
             function() {