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