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