f8e0d9c2d8e6e423b9aa948d93f8e853cf087842
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / circ / services / item.js
1 /**
2  * Shared item services for circulation
3  */
4
5 angular.module('egCoreMod')
6     .factory('egItem',
7        ['egCore','egCirc','$uibModal','$q','$timeout','$window','egConfirmDialog','egAlertDialog',
8 function(egCore , egCirc , $uibModal , $q , $timeout , $window , egConfirmDialog , egAlertDialog ) {
9
10     var service = {
11         copies : [], // copy barcode search results
12         index : 0 // search grid index
13     };
14
15     service.flesh = {   
16         flesh : 3, 
17         flesh_fields : {
18             acp : ['call_number','location','status','location','floating','circ_modifier',
19                 'age_protect','circ_lib','copy_alerts', 'editor', 'circ_as_type'],
20             acn : ['record','prefix','suffix','label_class'],
21             bre : ['simple_record','creator','editor']
22         },
23         select : { 
24             // avoid fleshing MARC on the bre
25             // note: don't add simple_record.. not sure why
26             bre : ['id','tcn_value','creator','editor'],
27         } 
28     }
29
30     service.circFlesh = {
31         flesh : 2,
32         flesh_fields : {
33             combcirc : [
34                 'usr',
35                 'workstation',
36                 'checkin_workstation',
37                 'checkin_lib',
38                 'duration_rule',
39                 'max_fine_rule',
40                 'recurring_fine_rule'
41             ],
42             au : ['card']
43         },
44         order_by : {combcirc : 'xact_start desc'},
45         limit :  1
46     }
47
48     //Retrieve separate copy, aacs, and accs information
49     service.getCopy = function(barcode, id) {
50         if (barcode) {
51             // handle barcode completion
52             return egCirc.handle_barcode_completion(barcode)
53             .then(function(actual_barcode) {
54                 return egCore.pcrud.search(
55                     'acp', {barcode : actual_barcode, deleted : 'f'},
56                     service.flesh).then(function(copy) {return copy});
57             });
58         }
59
60         return egCore.pcrud.retrieve( 'acp', id, service.flesh)
61             .then(function(copy) {return copy});
62     }
63
64     service.getCirc = function(id) {
65         return egCore.pcrud.search('combcirc', { target_copy : id },
66             service.circFlesh).then(function(circ) {return circ});
67     }
68
69     service.getSummary = function(id) {
70         return circ_summary = egCore.net.request(
71             'open-ils.circ',
72             'open-ils.circ.renewal_chain.retrieve_by_circ.summary',
73             egCore.auth.token(), id).then(
74                 function(circ_summary) {return circ_summary});
75     }
76
77     //Combine copy, circ, and accs information
78     service.retrieveCopyData = function(barcode, id) {
79         var copyData = {};
80
81         var fetchCopy = function(barcode, id) {
82             return service.getCopy(barcode, id)
83                 .then(function(copy) {
84                     copyData.copy = copy;
85                     return copyData;
86                 });
87         }
88         var fetchCirc = function(copy) {
89             return service.getCirc(copy.id())
90                 .then(function(circ) {
91                     copyData.circ = circ;
92                     return copyData;
93                 });
94         }
95         var fetchSummary = function(circ) {
96             return service.getSummary(circ.id())
97                 .then(function(summary) {
98                     copyData.circ_summary = summary;
99                     return copyData;
100                 });
101         }
102
103         return fetchCopy(barcode, id).then(function(res) {
104
105             if(!res.copy) { return $q.when(); }
106
107             return fetchCirc(copyData.copy).then(function(res) {
108                 if (copyData.circ) {
109                     return fetchSummary(copyData.circ).then(function() {
110                         return copyData;
111                     });
112                 } else {
113                     return copyData;
114                 }
115             });
116         });
117
118     }
119
120     // resolved with the last received copy
121     service.fetch = function(barcode, id, noListDupes) {
122         var copy;
123         var circ;
124         var circ_summary;
125         var lastRes = {};
126
127         return service.retrieveCopyData(barcode, id)
128         .then(function(copyData) {
129             if(!copyData) { return $q.when(); }
130             //Make sure we're getting a completed copyData - no plain acp or circ objects
131             if (copyData.circ) {
132                 // flesh circ_lib locally
133                 copyData.circ.circ_lib(egCore.org.get(copyData.circ.circ_lib()));
134
135             }
136             var flatCopy;
137
138             if (noListDupes) {
139                 // use the existing copy if possible
140                 flatCopy = service.copies.filter(
141                     function(c) {return c.id == copyData.copy.id()})[0];
142             }
143
144             // flesh acn.owning_lib
145             copyData.copy.call_number().owning_lib(egCore.org.get(copyData.copy.call_number().owning_lib()));
146
147             if (!flatCopy) {
148                 flatCopy = egCore.idl.toHash(copyData.copy, true);
149
150                 if (copyData.circ) {
151                     flatCopy._circ = egCore.idl.toHash(copyData.circ, true);
152                     flatCopy._circ_summary = egCore.idl.toHash(copyData.circ_summary, true);
153                     flatCopy._circ_lib = copyData.circ.circ_lib();
154                     flatCopy._duration = copyData.circ.duration();
155                     flatCopy._circ_ws = flatCopy._circ_summary.last_renewal_workstation ?
156                                         flatCopy._circ_summary.last_renewal_workstation :
157                                         flatCopy._circ_summary.checkout_workstation ?
158                                         flatCopy._circ_summary.checkout_workstation :
159                                         '';
160                 }
161                 flatCopy.index = service.index++;
162                 flatCopy.copy_alert_count = copyData.copy.copy_alerts().filter(function(aca) {
163                     return !aca.ack_time();
164                 }).length;
165
166                 service.copies.unshift(flatCopy);
167             }
168
169             //Get in-house use count
170             egCore.pcrud.search('aihu',
171                 {item : flatCopy.id}, {}, {idlist : true, atomic : true})
172             .then(function(uses) {
173                 flatCopy._inHouseUseCount = uses.length;
174                 copyData.copy._inHouseUseCount = uses.length;
175             });
176
177             //Get Monograph Parts
178             egCore.pcrud.search('acpm',
179                 {target_copy: flatCopy.id},
180                 { flesh : 1, flesh_fields : { acpm : ['part'] } },
181                 {atomic :true})
182             .then(function(acpm_array) {
183                 angular.forEach(acpm_array, function(acpm) {
184                     flatCopy.parts = egCore.idl.toHash(acpm.part());
185                     copyData.copy.parts = egCore.idl.toHash(acpm.part());
186                 });
187             });
188
189             return lastRes = {
190                 copy : copyData.copy,
191                 index : flatCopy.index
192             }
193         });
194
195
196     }
197
198     service.add_copies_to_bucket = function(copy_list) {
199         if (copy_list.length == 0) return;
200
201         return $uibModal.open({
202             templateUrl: './cat/catalog/t_add_to_bucket',
203             backdrop: 'static',
204             animation: true,
205             size: 'md',
206             controller:
207                    ['$scope','$uibModalInstance',
208             function($scope , $uibModalInstance) {
209
210                 $scope.bucket_id = 0;
211                 $scope.newBucketName = '';
212                 $scope.allBuckets = [];
213
214                 egCore.net.request(
215                     'open-ils.actor',
216                     'open-ils.actor.container.retrieve_by_class.authoritative',
217                     egCore.auth.token(), egCore.auth.user().id(),
218                     'copy', 'staff_client'
219                 ).then(function(buckets) { $scope.allBuckets = buckets; });
220
221                 $scope.add_to_bucket = function() {
222                     var promises = [];
223                     angular.forEach(copy_list, function (cp) {
224                         var item = new egCore.idl.ccbi()
225                         item.bucket($scope.bucket_id);
226                         item.target_copy(cp);
227                         promises.push(
228                             egCore.net.request(
229                                 'open-ils.actor',
230                                 'open-ils.actor.container.item.create',
231                                 egCore.auth.token(), 'copy', item
232                             )
233                         );
234
235                         return $q.all(promises).then(function() {
236                             $uibModalInstance.close();
237                         });
238                     });
239                 }
240
241                 $scope.add_to_new_bucket = function() {
242                     var bucket = new egCore.idl.ccb();
243                     bucket.owner(egCore.auth.user().id());
244                     bucket.name($scope.newBucketName);
245                     bucket.description('');
246                     bucket.btype('staff_client');
247
248                     return egCore.net.request(
249                         'open-ils.actor',
250                         'open-ils.actor.container.create',
251                         egCore.auth.token(), 'copy', bucket
252                     ).then(function(bucket) {
253                         $scope.bucket_id = bucket;
254                         $scope.add_to_bucket();
255                     });
256                 }
257
258                 $scope.cancel = function() {
259                     $uibModalInstance.dismiss();
260                 }
261             }]
262         });
263     }
264
265     service.make_copies_bookable = function(items) {
266
267         var copies_by_record = {};
268         var record_list = [];
269         angular.forEach(
270             items,
271             function (item) {
272                 var record_id = item['call_number.record.id'];
273                 if (typeof copies_by_record[ record_id ] == 'undefined') {
274                     copies_by_record[ record_id ] = [];
275                     record_list.push( record_id );
276                 }
277                 copies_by_record[ record_id ].push(item.id);
278             }
279         );
280
281         var promises = [];
282         var combined_results = [];
283         angular.forEach(record_list, function(record_id) {
284             promises.push(
285                 egCore.net.request(
286                     'open-ils.booking',
287                     'open-ils.booking.resources.create_from_copies',
288                     egCore.auth.token(),
289                     copies_by_record[record_id]
290                 ).then(function(results) {
291                     if (results && results['brsrc']) {
292                         combined_results = combined_results.concat(results['brsrc']);
293                     }
294                 })
295             );
296         });
297
298         $q.all(promises).then(function() {
299             if (combined_results.length > 0) {
300                 $uibModal.open({
301                     template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
302                     backdrop: 'static',
303                     animation: true,
304                     size: 'md',
305                     controller:
306                            ['$scope','$location','egCore','$uibModalInstance',
307                     function($scope , $location , egCore , $uibModalInstance) {
308
309                         $scope.funcs = {
310                             ses : egCore.auth.token(),
311                             resultant_brsrc : combined_results.map(function(o) { return o[0]; })
312                         }
313
314                         var booking_path = '/eg/conify/global/booking/resource';
315
316                         $scope.booking_admin_url =
317                             $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
318                     }]
319                 });
320             }
321         });
322     }
323
324     service.book_copies_now = function(items) {
325         var copies_by_record = {};
326         var record_list = [];
327         angular.forEach(
328             items,
329             function (item) {
330                 var record_id = item['call_number.record.id'];
331                 if (typeof copies_by_record[ record_id ] == 'undefined') {
332                     copies_by_record[ record_id ] = [];
333                     record_list.push( record_id );
334                 }
335                 copies_by_record[ record_id ].push(item.id);
336             }
337         );
338
339         var promises = [];
340         var combined_brt = [];
341         var combined_brsrc = [];
342         angular.forEach(record_list, function(record_id) {
343             promises.push(
344                 egCore.net.request(
345                     'open-ils.booking',
346                     'open-ils.booking.resources.create_from_copies',
347                     egCore.auth.token(),
348                     copies_by_record[record_id]
349                 ).then(function(results) {
350                     if (results && results['brt']) {
351                         combined_brt = combined_brt.concat(results['brt']);
352                     }
353                     if (results && results['brsrc']) {
354                         combined_brsrc = combined_brsrc.concat(results['brsrc']);
355                     }
356                 })
357             );
358         });
359
360         $q.all(promises).then(function() {
361             if (combined_brt.length > 0 || combined_brsrc.length > 0) {
362                 $uibModal.open({
363                     template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
364                     backdrop: 'static',
365                     animation: true,
366                     size: 'md',
367                     controller:
368                            ['$scope','$location','egCore','$uibModalInstance',
369                     function($scope , $location , egCore , $uibModalInstance) {
370
371                         $scope.funcs = {
372                             ses : egCore.auth.token(),
373                             bresv_interface_opts : {
374                                 booking_results : {
375                                      brt : combined_brt
376                                     ,brsrc : combined_brsrc
377                                 }
378                             }
379                         }
380
381                         var booking_path = '/eg/booking/reservation';
382
383                         $scope.booking_admin_url =
384                             $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
385
386                     }]
387                 });
388             }
389         });
390     }
391
392     service.requestItems = function(copy_list) {
393         if (copy_list.length == 0) return;
394
395         return $uibModal.open({
396             templateUrl: './cat/catalog/t_request_items',
397             backdrop: 'static',
398             animation: true,
399             controller:
400                    ['$scope','$uibModalInstance','egUser',
401             function($scope , $uibModalInstance , egUser) {
402                 $scope.user = null;
403                 $scope.first_user_fetch = true;
404
405                 $scope.hold_data = {
406                     hold_type : 'C',
407                     copy_list : copy_list,
408                     pickup_lib: egCore.org.get(egCore.auth.user().ws_ou()),
409                     user      : egCore.auth.user().id()
410                 };
411
412                 egUser.get( $scope.hold_data.user ).then(function(u) {
413                     $scope.user = u;
414                     $scope.barcode = u.card().barcode();
415                     $scope.user_name = egUser.format_name(u);
416                     $scope.hold_data.user = u.id();
417                 });
418
419                 $scope.user_name = '';
420                 $scope.barcode = '';
421                 $scope.$watch('barcode', function (n) {
422                     if (!$scope.first_user_fetch) {
423                         egUser.getByBarcode(n).then(function(u) {
424                             $scope.user = u;
425                             $scope.user_name = egUser.format_name(u);
426                             $scope.hold_data.user = u.id();
427                         }, function() {
428                             $scope.user = null;
429                             $scope.user_name = '';
430                             delete $scope.hold_data.user;
431                         });
432                     }
433                     $scope.first_user_fetch = false;
434                 });
435
436                 $scope.ok = function(h) {
437                     var args = {
438                         patronid  : h.user,
439                         hold_type : h.hold_type,
440                         pickup_lib: h.pickup_lib.id(),
441                         depth     : 0
442                     };
443
444                     egCore.net.request(
445                         'open-ils.circ',
446                         'open-ils.circ.holds.test_and_create.batch.override',
447                         egCore.auth.token(), args, h.copy_list
448                     );
449
450                     $uibModalInstance.close();
451                 }
452
453                 $scope.cancel = function($event) {
454                     $uibModalInstance.dismiss();
455                     $event.preventDefault();
456                 }
457             }]
458         });
459     }
460
461     service.attach_to_peer_bib = function(items) {
462         if (items.length == 0) return;
463
464         egCore.hatch.getItem('eg.cat.marked_conjoined_record').then(function(target_record) {
465             if (!target_record) return;
466
467             return $uibModal.open({
468                 templateUrl: './cat/catalog/t_conjoined_selector',
469                 backdrop: 'static',
470                 animation: true,
471                 controller:
472                        ['$scope','$uibModalInstance',
473                 function($scope , $uibModalInstance) {
474                     $scope.update = false;
475
476                     $scope.peer_type = null;
477                     $scope.peer_type_list = [];
478
479                     get_peer_types = function() {
480                         if (egCore.env.bpt)
481                             return $q.when(egCore.env.bpt.list);
482
483                         return egCore.pcrud.retrieveAll('bpt', null, {atomic : true})
484                         .then(function(list) {
485                             egCore.env.absorbList(list, 'bpt');
486                             return list;
487                         });
488                     }
489
490                     get_peer_types().then(function(list){
491                         $scope.peer_type_list = list;
492                     });
493
494                     $scope.ok = function(type) {
495                         var promises = [];
496
497                         angular.forEach(items, function (cp) {
498                             var n = new egCore.idl.bpbcm();
499                             n.isnew(true);
500                             n.peer_record(target_record);
501                             n.target_copy(cp.id);
502                             n.peer_type(type);
503                             promises.push(egCore.pcrud.create(n).then(function(){service.add_barcode_to_list(cp.barcode)}));
504                         });
505
506                         return $q.all(promises).then(function(){$uibModalInstance.close()});
507                     }
508
509                     $scope.cancel = function($event) {
510                         $uibModalInstance.dismiss();
511                         $event.preventDefault();
512                     }
513                 }]
514             });
515         });
516     }
517
518     service.selectedHoldingsCopyDelete = function (items) {
519         if (items.length == 0) return;
520
521         var copy_objects = [];
522         egCore.pcrud.search('acp',
523             {deleted : 'f', id : items.map(function(el){return el.id;}) },
524             { flesh : 1, flesh_fields : { acp : ['call_number'] } }
525         ).then(function() {
526
527             var cnHash = {};
528             var perCnCopies = {};
529
530             var cn_count = 0;
531             var cp_count = 0;
532
533             angular.forEach(
534                 copy_objects,
535                 function (cp) {
536                     cp.isdeleted(1);
537                     cp_count++;
538                     var cn_id = cp.call_number().id();
539                     if (!cnHash[cn_id]) {
540                         cnHash[cn_id] = cp.call_number();
541                         perCnCopies[cn_id] = [cp];
542                     } else {
543                         perCnCopies[cn_id].push(cp);
544                     }
545                     cp.call_number(cn_id); // prevent loops in JSON-ification
546                 }
547             );
548
549             angular.forEach(perCnCopies, function (v, k) {
550                 cnHash[k].copies(v);
551             });
552
553             cnList = [];
554             angular.forEach(cnHash, function (v, k) {
555                 cnList.push(v);
556             });
557
558             if (cnList.length == 0) return;
559
560             var flags = {};
561
562             egConfirmDialog.open(
563                 egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES,
564                 egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES_MESSAGE,
565                 {copies : cp_count, volumes : cn_count}
566             ).result.then(function() {
567                 egCore.net.request(
568                     'open-ils.cat',
569                     'open-ils.cat.asset.volume.fleshed.batch.update.override',
570                     egCore.auth.token(), cnList, 1, flags
571                 ).then(function(){
572                     angular.forEach(items, function(cp){service.add_barcode_to_list(cp.barcode)});
573                 });
574             });
575         },
576         null,
577         function(copy) {
578             copy_objects.push(copy);
579         });
580     }
581
582     service.checkin = function (items) {
583         angular.forEach(items, function (cp) {
584             egCirc.checkin({copy_barcode:cp.barcode}).then(
585                 function() { service.add_barcode_to_list(cp.barcode) }
586             );
587         });
588     }
589
590     service.renew = function (items) {
591         angular.forEach(items, function (cp) {
592             egCirc.renew({copy_barcode:cp.barcode}).then(
593                 function() { service.add_barcode_to_list(cp.barcode) }
594             );
595         });
596     }
597
598     service.cancel_transit = function (items) {
599         angular.forEach(items, function(cp) {
600             egCirc.find_copy_transit(null, {copy_barcode:cp.barcode})
601                 .then(function(t) { return egCirc.abort_transit(t.id())    })
602                 .then(function()  { return service.add_barcode_to_list(cp.barcode) });
603         });
604     }
605
606     service.selectedHoldingsDamaged = function (items) {
607         angular.forEach(items, function(cp) {
608             if (cp) {
609                 egCirc.mark_damaged({
610                     id: cp.id,
611                     barcode: cp.barcode,
612                     refresh: cp.refresh
613                 });
614             }
615         });
616     }
617
618     service.selectedHoldingsMissing = function (items) {
619         egCirc.mark_missing(items.map(function(el){return el.id;})).then(function(){
620             angular.forEach(items, function(cp){service.add_barcode_to_list(cp.barcode)});
621         });
622     }
623
624     service.gatherSelectedRecordIds = function (items) {
625         var rid_list = [];
626         angular.forEach(
627             items,
628             function (item) {
629                 if (rid_list.indexOf(item['call_number.record.id']) == -1)
630                     rid_list.push(item['call_number.record.id'])
631             }
632         );
633         return rid_list;
634     }
635
636     service.gatherSelectedVolumeIds = function (items,rid) {
637         var cn_id_list = [];
638         angular.forEach(
639             items,
640             function (item) {
641                 if (rid && item['call_number.record.id'] != rid) return;
642                 if (cn_id_list.indexOf(item['call_number.id']) == -1)
643                     cn_id_list.push(item['call_number.id'])
644             }
645         );
646         return cn_id_list;
647     }
648
649     service.gatherSelectedHoldingsIds = function (items,rid) {
650         var cp_id_list = [];
651         angular.forEach(
652             items,
653             function (item) {
654                 if (rid && item['call_number.record.id'] != rid) return;
655                 cp_id_list.push(item.id)
656             }
657         );
658         return cp_id_list;
659     }
660
661     service.spawnHoldingsAdd = function (items,use_vols,use_copies){
662         angular.forEach(service.gatherSelectedRecordIds(items), function (r) {
663             var raw = [];
664             if (use_copies) { // just a copy on existing volumes
665                 angular.forEach(service.gatherSelectedVolumeIds(items,r), function (v) {
666                     raw.push( {callnumber : v} );
667                 });
668             } else if (use_vols) {
669                 angular.forEach(
670                     service.gatherSelectedHoldingsIds(items,r),
671                     function (i) {
672                         angular.forEach(items, function(item) {
673                             if (i == item.id) raw.push({owner : item['call_number.owning_lib']});
674                         });
675                     }
676                 );
677             }
678
679             if (raw.length == 0) raw.push({});
680
681             egCore.net.request(
682                 'open-ils.actor',
683                 'open-ils.actor.anon_cache.set_value',
684                 null, 'edit-these-copies', {
685                     record_id: r,
686                     raw: raw,
687                     hide_vols : false,
688                     hide_copies : false
689                 }
690             ).then(function(key) {
691                 if (key) {
692                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
693                     $timeout(function() { $window.open(url, '_blank') });
694                 } else {
695                     alert('Could not create anonymous cache key!');
696                 }
697             });
698         });
699     }
700
701     service.spawnHoldingsEdit = function (items,hide_vols,hide_copies){
702         var item_ids = [];
703         angular.forEach(items, function(i){
704             item_ids.push(i.id);
705         });
706         
707         egCore.net.request(
708             'open-ils.actor',
709             'open-ils.actor.anon_cache.set_value',
710             null,
711             'edit-these-copies',
712             {
713                 record_id: 0,  // disables record summary
714                 copies: item_ids,
715                 raw: {},
716                 hide_vols : hide_vols,
717                 hide_copies : hide_copies
718             }).then(function(key) {
719                 if (key) {
720                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
721                     $timeout(function() { $window.open(url, '_blank') });
722                 } else {
723                     alert('Could not create anonymous cache key!');
724                 }
725             });
726     }
727
728     service.replaceBarcodes = function(items) {
729         angular.forEach(items, function (cp) {
730             $uibModal.open({
731                 templateUrl: './cat/share/t_replace_barcode',
732                 backdrop: 'static',
733                 animation: true,
734                 controller:
735                            ['$scope','$uibModalInstance',
736                     function($scope , $uibModalInstance) {
737                         $scope.isModal = true;
738                         $scope.focusBarcode = false;
739                         $scope.focusBarcode2 = true;
740                         $scope.barcode1 = cp.barcode;
741
742                         $scope.updateBarcode = function() {
743                             $scope.copyNotFound = false;
744                             $scope.updateOK = false;
745
746                             egCore.pcrud.search('acp',
747                                 {deleted : 'f', barcode : $scope.barcode1})
748                             .then(function(copy) {
749
750                                 if (!copy) {
751                                     $scope.focusBarcode = true;
752                                     $scope.copyNotFound = true;
753                                     return;
754                                 }
755
756                                 $scope.copyId = copy.id();
757                                 copy.barcode($scope.barcode2);
758
759                                 egCore.pcrud.update(copy).then(function(stat) {
760                                     $scope.updateOK = stat;
761                                     $scope.focusBarcode = true;
762                                     if (stat) service.add_barcode_to_list(copy.barcode());
763                                 });
764
765                             });
766                             $uibModalInstance.close();
767                         }
768
769                         $scope.cancel = function($event) {
770                             $uibModalInstance.dismiss();
771                             $event.preventDefault();
772                         }
773                     }
774                 ]
775             });
776         });
777     }
778
779     // this "transfers" selected copies to a new owning library,
780     // auto-creating volumes and deleting unused volumes as required.
781     service.changeItemOwningLib = function(items) {
782         var xfer_target = egCore.hatch.getLocalItem('eg.cat.transfer_target_lib');
783         if (!xfer_target || !items.length) {
784             return;
785         }
786         var vols_to_move   = {};
787         var copies_to_move = {};
788         angular.forEach(items, function(item) {
789             if (item['call_number.owning_lib'] != xfer_target) {
790                 if (item['call_number.id'] in vols_to_move) {
791                     copies_to_move[item['call_number.id']].push(item.id);
792                 } else {
793                     vols_to_move[item['call_number.id']] = {
794                         label       : item['call_number.label'],
795                         label_class : item['call_number.label_class'],
796                         record      : item['call_number.record.id'],
797                         prefix      : item['call_number.prefix.id'],
798                         suffix      : item['call_number.suffix.id']
799                     };
800                     copies_to_move[item['call_number.id']] = new Array;
801                     copies_to_move[item['call_number.id']].push(item.id);
802                 }
803             }
804         });
805
806         var promises = [];
807         angular.forEach(vols_to_move, function(vol, vol_id) {
808             promises.push(egCore.net.request(
809                 'open-ils.cat',
810                 'open-ils.cat.call_number.find_or_create',
811                 egCore.auth.token(),
812                 vol.label,
813                 vol.record,
814                 xfer_target,
815                 vol.prefix,
816                 vol.suffix,
817                 vol.label_class
818             ).then(function(resp) {
819                 var evt = egCore.evt.parse(resp);
820                 if (evt) return;
821                 return egCore.net.request(
822                     'open-ils.cat',
823                     'open-ils.cat.transfer_copies_to_volume',
824                     egCore.auth.token(),
825                     resp.acn_id,
826                     copies_to_move[vol_id]
827                 );
828             }));
829         });
830
831         $q.all(promises)
832         .then(
833             function() {
834                 angular.forEach(items, function(cp){service.add_barcode_to_list(cp.barcode)});
835             }
836         );
837     }
838
839     service.transferItems = function (items){
840         var xfer_target = egCore.hatch.getLocalItem('eg.cat.transfer_target_vol');
841         var copy_ids = service.gatherSelectedHoldingsIds(items);
842         if (xfer_target && copy_ids.length > 0) {
843             egCore.net.request(
844                 'open-ils.cat',
845                 'open-ils.cat.transfer_copies_to_volume',
846                 egCore.auth.token(),
847                 xfer_target,
848                 copy_ids
849             ).then(
850                 function(resp) { // oncomplete
851                     var evt = egCore.evt.parse(resp);
852                     if (evt) {
853                         egConfirmDialog.open(
854                             egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE,
855                             egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY,
856                             {'evt_desc': evt}
857                         ).result.then(function() {
858                             egCore.net.request(
859                                 'open-ils.cat',
860                                 'open-ils.cat.transfer_copies_to_volume.override',
861                                 egCore.auth.token(),
862                                 xfer_target,
863                                 copy_ids,
864                                 { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
865                             );
866                         }).then(function() {
867                             angular.forEach(items, function(cp){service.add_barcode_to_list(cp.barcode)});
868                         });
869                     } else {
870                         angular.forEach(items, function(cp){service.add_barcode_to_list(cp.barcode)});
871                     }
872
873                 },
874                 null, // onerror
875                 null // onprogress
876             );
877         }
878     }
879
880     service.mark_missing_pieces = function(copy,outer_scope) {
881         var b = copy.barcode();
882         var t = egCore.idl.toHash(copy.call_number()).record.title;
883         egConfirmDialog.open(
884             egCore.strings.CONFIRM_MARK_MISSING_TITLE,
885             egCore.strings.CONFIRM_MARK_MISSING_BODY,
886             { barcode : b, title : t }
887         ).result.then(function() {
888
889             // kick off mark missing
890             return egCore.net.request(
891                 'open-ils.circ',
892                 'open-ils.circ.mark_item_missing_pieces',
893                 egCore.auth.token(), copy.id()
894             )
895
896         }).then(function(resp) {
897             var evt = egCore.evt.parse(resp); // should always produce event
898
899             if (evt.textcode == 'ACTION_CIRCULATION_NOT_FOUND') {
900                 return egAlertDialog.open(
901                     egCore.strings.CIRC_NOT_FOUND, {barcode : copy.barcode()});
902             }
903
904             var payload = evt.payload;
905
906             // TODO: open copy editor inline?  new tab?
907
908             // print the missing pieces slip
909             var promise = $q.when();
910             if (payload.slip) {
911                 // wait for completion, since it may spawn a confirm dialog
912                 promise = egCore.print.print({
913                     context : 'default',
914                     content_type : 'text/html',
915                     content : payload.slip.template_output().data()
916                 });
917             }
918
919             if (payload.letter) {
920                 outer_scope.letter = payload.letter.template_output().data();
921             }
922
923             // apply patron penalty
924             if (payload.circ) {
925                 promise.then(function() {
926                     egCirc.create_penalty(payload.circ.usr())
927                 });
928             }
929
930         });
931     }
932
933     service.print_spine_labels = function(copy_ids){
934         egCore.net.request(
935             'open-ils.actor',
936             'open-ils.actor.anon_cache.set_value',
937             null, 'print-labels-these-copies', {
938                 copies : copy_ids
939             }
940         ).then(function(key) {
941             if (key) {
942                 var url = egCore.env.basePath + 'cat/printlabels/' + key;
943                 $timeout(function() { $window.open(url, '_blank') });
944             } else {
945                 alert('Could not create anonymous cache key!');
946             }
947         });
948     }
949
950     return service;
951 }])
952 .filter('string_pick', function() { return function(i){ return arguments[i] || ''; }; })