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