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