]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/circ/services/circ.js
LP1915464 follow-up: use spaces, not tabs; remove extra comma
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / circ / services / circ.js
1 /**
2  * Checkin, checkout, and renew
3  */
4
5 angular.module('egCoreMod')
6
7 .factory('egCirc',
8
9        ['$uibModal','$q','egCore','egAlertDialog','egConfirmDialog','egAddCopyAlertDialog','egCopyAlertManagerDialog','egCopyAlertEditorDialog',
10         'egWorkLog',
11 function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAlertDialog , egCopyAlertManagerDialog,  egCopyAlertEditorDialog ,
12          egWorkLog) {
13
14     var service = {
15         // auto-override these events after the first override
16         auto_override_circ_events : {},
17         // auto-skip these events after the first skip
18         auto_skip_circ_events : {},
19         require_initials : false,
20         never_auto_print : {
21             hold_shelf_slip : false,
22             hold_transit_slip : false,
23             transit_slip : false
24         },
25         in_flight_checkins: {}
26     };
27
28     egCore.startup.go().finally(function() {
29         egCore.org.settings([
30             'ui.staff.require_initials.patron_standing_penalty',
31             'ui.admin.work_log.max_entries',
32             'ui.admin.patron_log.max_entries',
33             'circ.staff_client.do_not_auto_attempt_print',
34             'circ.clear_hold_on_checkout'
35         ]).then(function(set) {
36             service.require_initials = Boolean(set['ui.staff.require_initials.patron_standing_penalty']);
37             service.clearHold = Boolean(set['circ.clear_hold_on_checkout']);
38
39             if (angular.isArray(set['circ.staff_client.do_not_auto_attempt_print'])) {
40                 if (set['circ.staff_client.do_not_auto_attempt_print'].indexOf('Hold Slip') > 1)
41                     service.never_auto_print['hold_shelf_slip'] = true;
42                 if (set['circ.staff_client.do_not_auto_attempt_print'].indexOf('Hold/Transit Slip') > 1)
43                     service.never_auto_print['hold_transit_slip'] = true;
44                 if (set['circ.staff_client.do_not_auto_attempt_print'].indexOf('Transit Slip') > 1)
45                     service.never_auto_print['transit_slip'] = true;
46             }
47         });
48     });
49
50     service.reset = function() {
51         service.auto_override_circ_events = {};
52         service.auto_skip_circ_events = {};
53     }
54
55     // these events cannot be overriden
56     service.nonoverridable_events = [
57         'ACTION_CIRCULATION_NOT_FOUND',
58         'ACTOR_USER_NOT_FOUND',
59         'ASSET_COPY_NOT_FOUND',
60         'PATRON_INACTIVE',
61         'PATRON_CARD_INACTIVE',
62         'PATRON_ACCOUNT_EXPIRED',
63         'PERM_FAILURE' // should be handled elsewhere
64     ]
65
66     // Default to checked for "Automatically override for subsequent items?"
67     service.default_auto_override = [
68         'PATRON_EXCEEDS_OVERDUE_COUNT',
69         'PATRON_BARRED',
70         'PATRON_EXCEEDS_LOST_COUNT',
71         'PATRON_EXCEEDS_CHECKOUT_COUNT',
72         'PATRON_EXCEEDS_FINES',
73         'PATRON_EXCEEDS_LONGOVERDUE_COUNT'
74     ]
75
76     // these checkin events do not produce alerts when 
77     // options.suppress_alerts is in effect.
78     service.checkin_suppress_overrides = [
79         'COPY_BAD_STATUS',
80         'PATRON_BARRED',
81         'PATRON_INACTIVE',
82         'PATRON_ACCOUNT_EXPIRED',
83         'ITEM_DEPOSIT_PAID',
84         'CIRC_CLAIMS_RETURNED',
85         'COPY_ALERT_MESSAGE',
86         'COPY_STATUS_LOST',
87         'COPY_STATUS_LOST_AND_PAID',
88         'COPY_STATUS_LONG_OVERDUE',
89         'COPY_STATUS_MISSING',
90         'PATRON_EXCEEDS_FINES'
91     ]
92
93     // these events can be overridden by staff during checkin
94     service.checkin_overridable_events = 
95         service.checkin_suppress_overrides.concat([
96         'TRANSIT_CHECKIN_INTERVAL_BLOCK'
97     ])
98
99     // Performs a checkout.
100     // Returns a promise resolved with the original params and options
101     // and the final checkout event (e.g. in the case of override).
102     // Rejected if the checkout cannot be completed.
103     //
104     // params : passed directly as arguments to the server API 
105     // options : non-parameter controls.  e.g. "override", "check_barcode"
106     service.checkout = function(params, options) {
107         if (!options) options = {};
108         params.new_copy_alerts = 1;
109
110         console.debug('egCirc.checkout() : ' 
111             + js2JSON(params) + ' : ' + js2JSON(options));
112
113         // handle barcode completion
114         return service.handle_barcode_completion(params.copy_barcode)
115         .then(function(barcode) {
116             console.debug('barcode after completion: ' + barcode);
117             params.copy_barcode = barcode;
118
119             var promise = options.check_barcode ? 
120                 service.test_barcode(params.copy_barcode) : $q.when();
121
122             // avoid re-check on override, etc.
123             delete options.check_barcode;
124
125             return promise.then(function() {
126
127                 var method = 'open-ils.circ.checkout.full';
128                 if (options.override) method += '.override';
129
130                 return egCore.net.request(
131                     'open-ils.circ', method, egCore.auth.token(), params
132
133                 ).then(function(evt) {
134
135                     if (!angular.isArray(evt)) evt = [evt];
136
137                     if (evt[0].payload && evt[0].payload.auto_renew == 1) {
138                         // open circulation found with auto-renew toggle on.
139                         console.debug('Auto-renewing item ' + params.copy_barcode);
140                         options.auto_renew = true;
141                         return service.renew(params, options);
142                     }
143
144                     var action = params.noncat ? 'noncat_checkout' : 'checkout';
145
146                     return service.flesh_response_data(action, evt, params, options)
147                     .then(function() {
148                         return service.handle_checkout_resp(evt, params, options);
149                     })
150                     .then(function(final_resp) {
151                         return service.munge_resp_data(final_resp,action,method)
152                     })
153                 });
154             });
155         });
156     }
157
158     // Performs a renewal.
159     // Returns a promise resolved with the original params and options
160     // and the final checkout event (e.g. in the case of override)
161     // Rejected if the renewal cannot be completed.
162     service.renew = function(params, options) {
163         if (!options) options = {};
164         params.new_copy_alerts = 1;
165
166         console.debug('egCirc.renew() : ' 
167             + js2JSON(params) + ' : ' + js2JSON(options));
168
169         // handle barcode completion
170         return service.handle_barcode_completion(params.copy_barcode)
171         .then(function(barcode) {
172             params.copy_barcode = barcode;
173
174             var promise = options.check_barcode ? 
175                 service.test_barcode(params.copy_barcode) : $q.when();
176
177             // avoid re-check on override, etc.
178             delete options.check_barcode;
179
180             return promise.then(function() {
181
182                 var method = 'open-ils.circ.renew';
183                 if (options.override) method += '.override';
184
185                 return egCore.net.request(
186                     'open-ils.circ', method, egCore.auth.token(), params
187
188                 ).then(function(evt) {
189
190                     if (!angular.isArray(evt)) evt = [evt];
191
192                     return service.flesh_response_data(
193                         'renew', evt, params, options)
194                     .then(function() {
195                         return service.handle_renew_resp(evt, params, options);
196                     })
197                     .then(function(final_resp) {
198                         final_resp.auto_renew = options.auto_renew;
199                         return service.munge_resp_data(final_resp,'renew',method)
200                     })
201                 });
202             });
203         });
204     }
205
206     // Performs a checkin
207     // Returns a promise resolved with the original params and options,
208     // plus the final checkin event (e.g. in the case of override).
209     // Rejected if the checkin cannot be completed.
210     service.checkin = function(params, options) {
211         if (!options) options = {};
212         params.new_copy_alerts = 1;
213
214         console.debug('egCirc.checkin() : ' 
215             + js2JSON(params) + ' : ' + js2JSON(options));
216
217         // handle barcode completion
218         return service.handle_barcode_completion(params.copy_barcode)
219         .then(function(barcode) {
220             params.copy_barcode = barcode;
221
222             var promise = options.check_barcode ? 
223                 service.test_barcode(params.copy_barcode) : $q.when();
224
225             // avoid re-check on override, etc.
226             delete options.check_barcode;
227
228             return promise.then(function() {
229
230                 var method = 'open-ils.circ.checkin';
231                 if (options.override) method += '.override';
232
233                 // Multiple checkin API calls should never be active
234                 // for a single barcode.
235                 if (service.in_flight_checkins[barcode]) {
236                     console.error('Barcode ' + barcode 
237                         + ' is already in flight for checkin, skipping');
238                     return $q.reject();
239                 }
240                 service.in_flight_checkins[barcode] = true;
241
242                 return egCore.net.request(
243                     'open-ils.circ', method, egCore.auth.token(), params
244
245                 ).then(function(evt) {
246                     delete service.in_flight_checkins[barcode];
247
248                     if (!angular.isArray(evt)) evt = [evt];
249                     return service.flesh_response_data(
250                         'checkin', evt, params, options)
251                     .then(function() {
252                         return service.handle_checkin_resp(evt, params, options);
253                     })
254                     .then(function(final_resp) {
255                         return service.munge_resp_data(final_resp,'checkin',method)
256                     })
257                 }, function() {delete service.in_flight_checkins[barcode]});
258             });
259         });
260     }
261
262     // provide consistent formatting of the final response data
263     service.munge_resp_data = function(final_resp,worklog_action,worklog_method) {
264         var data = final_resp.data = {};
265
266         if (!final_resp.evt[0]) {
267             egCore.audio.play('error.unknown.no_event');
268             return;
269         }
270
271         var payload = final_resp.evt[0].payload;
272         if (!payload) {
273             egCore.audio.play('error.unknown.no_payload');
274             return;
275         }
276
277         // retrieve call number affixes prior to sending payload data to the grid
278         if (payload.volume && typeof payload.volume.prefix() != 'object') {
279             egCore.pcrud.retrieve('acnp',payload.volume.prefix()).then(function(p) {payload.volume.prefix(p)});
280         };
281         if (payload.volume && typeof payload.volume.suffix() != 'object') {
282             egCore.pcrud.retrieve('acns',payload.volume.suffix()).then(function(s) {payload.volume.suffix(s)});
283         };
284
285         data.circ = payload.circ;
286         data.parent_circ = payload.parent_circ;
287         data.hold = payload.hold;
288         data.record = payload.record;
289         data.acp = payload.copy;
290         data.acn = payload.volume ?  payload.volume : payload.copy ? payload.copy.call_number() : null;
291         data.au = payload.patron;
292         data.transit = payload.transit;
293         data.status = payload.status;
294         data.message = payload.message;
295         data.title = final_resp.evt[0].title;
296         data.author = final_resp.evt[0].author;
297         data.isbn = final_resp.evt[0].isbn;
298         data.route_to = final_resp.evt[0].route_to;
299
300
301         if (payload.circ) data.duration = payload.circ.duration();
302         if (payload.circ) data.circ_lib = payload.circ.circ_lib();
303
304         // for checkin, the mbts lives on the main circ
305         if (payload.circ && payload.circ.billable_transaction())
306             data.mbts = payload.circ.billable_transaction().summary();
307
308         // on renewals, the mbts lives on the parent circ
309         if (payload.parent_circ && payload.parent_circ.billable_transaction())
310             data.mbts = payload.parent_circ.billable_transaction().summary();
311
312         if (!data.route_to) {
313             if (data.transit && !data.transit.dest_recv_time() && !data.transit.cancel_time()) {
314                 data.route_to = data.transit.dest().shortname();
315             } else if (data.acp) {
316                 data.route_to = data.acp.location().name();
317             }
318         }
319         // allow us to get at the monograph parts associated with a copy
320         if (payload.copy && payload.copy.parts()) {
321             data._monograph_part = payload.copy.parts().map(function(part) {
322                 return part.label();
323             }).join(',');
324         }
325
326         egWorkLog.record(
327             (worklog_action == 'checkout' || worklog_action == 'noncat_checkout')
328             ? egCore.strings.EG_WORK_LOG_CHECKOUT
329             : (worklog_action == 'renew'
330                 ? egCore.strings.EG_WORK_LOG_RENEW
331                 : egCore.strings.EG_WORK_LOG_CHECKIN // worklog_action == 'checkin'
332             ),{
333                 'action' : worklog_action,
334                 'method' : worklog_method,
335                 'response' : final_resp
336             }
337         );
338
339         return final_resp;
340     }
341
342     service.handle_overridable_checkout_event = function(evt, params, options) {
343
344         if (options.override) {
345             // override attempt already made and failed.
346             // NOTE: I don't think we'll ever get here, since the
347             // override attempt should produce a perm failure...
348             angular.forEach(evt, function(e){ console.debug('override failed: ' + e.textcode); });
349             return $q.reject();
350
351         } 
352
353         if (evt.filter(function(e){return !service.auto_override_circ_events[e.textcode];}).length == 0) {
354             // user has already opted to override these type
355             // of events.  Re-run the checkout w/ override.
356             options.override = true;
357             return service.checkout(params, options);
358         } 
359
360         // Ask the user if they would like to override this event.
361         // Some events offer a stock override dialog, while others
362         // require additional context.
363
364         switch(evt[0].textcode) {
365             case 'COPY_NOT_AVAILABLE':
366                 return service.copy_not_avail_dialog(evt, params, options);
367             case 'COPY_ALERT_MESSAGE':
368                 return service.copy_alert_dialog(evt[0], params, options, 'checkout');
369             default: 
370                 return service.override_dialog(evt, params, options, 'checkout');
371         }
372     }
373
374     service.handle_overridable_renew_event = function(evt, params, options) {
375
376         if (options.override) {
377             // override attempt already made and failed.
378             // NOTE: I don't think we'll ever get here, since the
379             // override attempt should produce a perm failure...
380             angular.forEach(evt, function(e){ console.debug('override failed: ' + e.textcode); });
381             return $q.reject();
382
383         } 
384
385         // renewal auto-overrides are the same as checkout
386         if (evt.filter(function(e){return !service.auto_override_circ_events[e.textcode];}).length == 0) {
387             // user has already opted to override these type
388             // of events.  Re-run the renew w/ override.
389             options.override = true;
390             return service.renew(params, options);
391         } 
392
393         // Ask the user if they would like to override this event.
394         // Some events offer a stock override dialog, while others
395         // require additional context.
396
397         switch(evt[0].textcode) {
398             case 'COPY_ALERT_MESSAGE':
399                 return service.copy_alert_dialog(evt[0], params, options, 'renew');
400             default: 
401                 return service.override_dialog(evt, params, options, 'renew');
402         }
403     }
404
405
406     service.handle_overridable_checkin_event = function(evt, params, options) {
407
408         if (options.override) {
409             // override attempt already made and failed.
410             // NOTE: I don't think we'll ever get here, since the
411             // override attempt should produce a perm failure...
412             angular.forEach(evt, function(e){ console.debug('override failed: ' + e.textcode); });
413             return $q.reject();
414
415         } 
416
417         if (options.suppress_popups
418             && evt.filter(function(e){return service.checkin_suppress_overrides.indexOf(e.textcode) == -1;}).length == 0) {
419             // Events are suppressed.  Re-run the checkin w/ override.
420             options.override = true;
421             return service.checkin(params, options);
422         } 
423
424         // Ask the user if they would like to override this event.
425         // Some events offer a stock override dialog, while others
426         // require additional context.
427
428         switch(evt[0].textcode) {
429             case 'COPY_ALERT_MESSAGE':
430                 return service.copy_alert_dialog(evt[0], params, options, 'checkin');
431             default: 
432                 return service.override_dialog(evt, params, options, 'checkin');
433         }
434     }
435
436
437     service.handle_renew_resp = function(evt, params, options) {
438
439         var final_resp = {evt : evt, params : params, options : options};
440
441         // track the barcode regardless of whether it refers to a copy
442         angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
443
444         // test for success first to simplify things
445         if (evt[0].textcode == 'SUCCESS') {
446             egCore.audio.play('info.renew');
447             return $q.when(final_resp);
448         }
449
450         // handle Overridable and Non-Overridable Events, but only if no skipped non-overridable events
451         if (evt.filter(function(e){return service.auto_skip_circ_events[e.textcode];}).length == 0) {
452             return service.handle_overridable_renew_event(evt, params, options);
453         }
454
455         // Other events
456         switch (evt[0].textcode) {
457             case 'COPY_IN_TRANSIT':
458             case 'PATRON_CARD_INACTIVE':
459             case 'PATRON_INACTIVE':
460             case 'PATRON_ACCOUNT_EXPIRED':
461             case 'CIRC_CLAIMS_RETURNED':
462             case 'ITEM_NOT_CATALOGED':
463             case 'ASSET_COPY_NOT_FOUND':
464                 // since handle_overridable_renew_event essentially advertises these events at some point,
465                 // we no longer need the original alerts; however, the sound effects are still nice.
466                 egCore.audio.play('warning.renew');
467                 return $q.reject();
468
469             default:
470                 egCore.audio.play('warning.renew.unknown');
471                 return service.exit_alert(
472                     egCore.strings.CHECKOUT_FAILED_GENERIC, {
473                         barcode : params.copy_barcode,
474                         textcode : evt[0].textcode,
475                         desc : evt[0].desc
476                     }
477                 );
478         }
479     }
480
481
482     service.handle_checkout_resp = function(evt, params, options) {
483
484         var final_resp = {evt : evt, params : params, options : options};
485
486         // track the barcode regardless of whether it refers to a copy
487         angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
488
489         // test for success first to simplify things
490         if (evt[0].textcode == 'SUCCESS') {
491             egCore.audio.play('success.checkout');
492             return $q.when(final_resp);
493         }
494
495         // other events that should precede generic overridable/non-overridable handling
496         switch (evt[0].textcode) {
497             case 'ITEM_NOT_CATALOGED':
498                 egCore.audio.play('error.checkout.no_cataloged');
499                 return service.precat_dialog(params, options);
500
501             case 'OPEN_CIRCULATION_EXISTS':
502                 // auto_renew checked in service.checkout()
503                 egCore.audio.play('error.checkout.open_circ');
504                 return service.circ_exists_dialog(evt, params, options);
505
506             case 'COPY_IN_TRANSIT':
507                 egCore.audio.play('warning.checkout.in_transit');
508                 return service.copy_in_transit_dialog(evt, params, options);
509         }
510
511         // handle Overridable and Non-Overridable Events, but only if no skipped non-overridable events
512         if (evt.filter(function(e){return service.auto_skip_circ_events[e.textcode];}).length == 0) {
513             return service.handle_overridable_checkout_event(evt, params, options);
514         }
515
516         // Other events
517         switch (evt[0].textcode) {
518             case 'PATRON_CARD_INACTIVE':
519             case 'PATRON_INACTIVE':
520             case 'PATRON_ACCOUNT_EXPIRED':
521             case 'CIRC_CLAIMS_RETURNED':
522             case 'ITEM_NOT_CATALOGED':
523             case 'ASSET_COPY_NOT_FOUND':
524                 // since handle_overridable_checkout_event essentially advertises these events at some point,
525                 // we no longer need the original alerts; however, the sound effects are still nice.
526                 egCore.audio.play('warning.checkout');
527                 return $q.reject();
528
529             default:
530                 egCore.audio.play('error.checkout.unknown');
531                 return service.exit_alert(
532                     egCore.strings.CHECKOUT_FAILED_GENERIC, {
533                         barcode : params.copy_barcode,
534                         textcode : evt[0].textcode,
535                         desc : evt[0].desc
536                     }
537                 );
538         }
539     }
540
541     // returns a promise resolved with the list of circ mods
542     service.get_circ_mods = function() {
543         if (egCore.env.ccm) 
544             return $q.when(egCore.env.ccm.list);
545
546         return egCore.pcrud.retrieveAll('ccm', null, {atomic : true})
547         .then(function(list) { 
548             egCore.env.absorbList(list, 'ccm');
549             return list;
550         });
551     };
552
553     // returns a promise resolved with the list of noncat types
554     service.get_noncat_types = function() {
555         if (egCore.env.cnct) 
556             return $q.when(egCore.env.cnct.list);
557
558         return egCore.pcrud.search('cnct', 
559             {owning_lib : 
560                 egCore.org.fullPath(egCore.auth.user().ws_ou(), true)}, 
561             null, {atomic : true}
562         ).then(function(list) { 
563             egCore.env.absorbList(list, 'cnct');
564             return list;
565         });
566     }
567
568     service.get_all_penalty_types = function() {
569         if (egCore.env.csp) 
570             return $q.when(egCore.env.csp.list);
571         return egCore.pcrud.retrieveAll('csp', {}, {atomic : true}).then(
572             function(penalties) {
573                 return egCore.env.absorbList(penalties, 'csp').list;
574             }
575         );
576     }
577
578     // ideally all of these data should be returned with the response,
579     // but until then, grab what we need.
580     service.flesh_response_data = function(action, evt, params, options) {
581         var promises = [];
582         var payload;
583         if (!evt[0] || !(payload = evt[0].payload)) return $q.when();
584         
585         promises.push(service.flesh_copy_location(payload.copy));
586         if (payload.copy) {
587             promises.push(service.flesh_acn_owning_lib(payload.volume));
588             promises.push(service.flesh_copy_circ_library(payload.copy));
589             promises.push(service.flesh_copy_circ_modifier(payload.copy));
590             promises.push(
591                 service.flesh_copy_status(payload.copy)
592
593                 .then(function() {
594                     // copy is in transit, but no transit was delivered
595                     // in the payload.  Do this here instead of below to
596                     // ensure consistent copy status fleshiness
597                     if (!payload.transit && payload.copy.status().id() == 6) { // in-transit
598                         return service.find_copy_transit(evt, params, options)
599                         .then(function(trans) {
600                             if (trans) {
601                                 trans.source(egCore.org.get(trans.source()));
602                                 trans.dest(egCore.org.get(trans.dest()));
603                                 payload.transit = trans;
604                             }
605                         })
606                     }
607                 })
608             );
609         }
610
611         // local flesh transit
612         if (transit = payload.transit) {
613             transit.source(egCore.org.get(transit.source()));
614             transit.dest(egCore.org.get(transit.dest()));
615         } 
616
617         // TODO: renewal responses should include the patron
618         if (!payload.patron) {
619             var user_id;
620             if (payload.circ) user_id = payload.circ.usr();
621             if (payload.noncat_circ) user_id = payload.noncat_circ.patron();
622             if (user_id) {
623                 promises.push(
624                     egCore.pcrud.retrieve('au', user_id)
625                     .then(function(user) {payload.patron = user})
626                 );
627             }
628         }
629
630         // extract precat values
631         angular.forEach(evt, function(e){ e.title = payload.record ? payload.record.title() : 
632             (payload.copy ? payload.copy.dummy_title() : null);});
633
634         angular.forEach(evt, function(e){ e.author = payload.record ? payload.record.author() : 
635             (payload.copy ? payload.copy.dummy_author() : null);});
636
637         angular.forEach(evt, function(e){ e.isbn = payload.record ? payload.record.isbn() : 
638             (payload.copy ? payload.copy.dummy_isbn() : null);});
639
640         return $q.all(promises);
641     }
642
643     service.flesh_acn_owning_lib = function(acn) {
644         if (!acn) return $q.when();
645         return $q.when(acn.owning_lib(egCore.org.get( acn.owning_lib() )));
646     }
647
648     service.flesh_copy_circ_library = function(copy) {
649         if (!copy) return $q.when();
650         
651         return $q.when(copy.circ_lib(egCore.org.get( copy.circ_lib() )));
652     }
653
654     // fetches the full list of circ modifiers
655     service.flesh_copy_circ_modifier = function(copy) {
656         if (!copy) return $q.when();
657         if (egCore.env.ccm)
658             return $q.when(copy.circ_modifier(egCore.env.ccm.map[copy.circ_modifier()]));
659         return egCore.pcrud.retrieveAll('ccm', {}, {atomic : true}).then(
660             function(list) {
661                 egCore.env.absorbList(list, 'ccm');
662                 copy.circ_modifier(egCore.env.ccm.map[copy.circ_modifier()]);
663             }
664         );
665     }
666
667     // fetches the full list of copy statuses
668     service.flesh_copy_status = function(copy) {
669         if (!copy) return $q.when();
670         if (egCore.env.ccs) 
671             return $q.when(copy.status(egCore.env.ccs.map[copy.status()]));
672         return egCore.pcrud.retrieveAll('ccs', {}, {atomic : true}).then(
673             function(list) {
674                 egCore.env.absorbList(list, 'ccs');
675                 copy.status(egCore.env.ccs.map[copy.status()]);
676             }
677         );
678     }
679
680     // there may be *many* copy locations and we may be handling items
681     // for other locations.  Fetch copy locations as-needed and cache.
682     service.flesh_copy_location = function(copy) {
683         if (!copy) return $q.when();
684         if (angular.isObject(copy.location())) return $q.when(copy);
685         if (egCore.env.acpl) {
686             if (egCore.env.acpl.map[copy.location()]) {
687                 copy.location(egCore.env.acpl.map[copy.location()]);
688                 return $q.when(copy);
689             }
690         } 
691         return egCore.pcrud.retrieve('acpl', copy.location())
692         .then(function(loc) {
693             egCore.env.absorbList([loc], 'acpl'); // append to cache
694             copy.location(loc);
695             return copy;
696         });
697     }
698
699
700     // fetch org unit addresses as needed.
701     service.get_org_addr = function(org_id, addr_type) {
702         var org = egCore.org.get(org_id);
703         var addr_id = org[addr_type]();
704
705         if (!addr_id) return $q.when(null);
706
707         if (egCore.env.aoa && egCore.env.aoa.map[addr_id]) 
708             return $q.when(egCore.env.aoa.map[addr_id]); 
709
710         return egCore.pcrud.retrieve('aoa', addr_id).then(function(addr) {
711             egCore.env.absorbList([addr], 'aoa');
712             return egCore.env.aoa.map[addr_id]; 
713         });
714     }
715     
716     //retrieve addresses from multiple org units
717     service.cache_org_addr = function (org_ids, addr_type) {
718         var addr_ids = [];
719         org_ids.forEach(function(org_id){
720             var org = egCore.org.get(org_id);
721             var addr_id = org[addr_type](); 
722             if(addr_id)addr_ids.push(addr_id);
723         });
724                 if (!addr_ids.length) return $q.when(null);
725         return egCore.pcrud.search('aoa', {id: addr_ids},{},{ atomic: true}).then(function(addrs) {
726             return egCore.env.absorbList(addrs, 'aoa');
727         });
728     }
729
730     service.exit_alert = function(msg, scope) {
731         return egAlertDialog.open(msg, scope).result.then(
732             function() {return $q.reject()});
733     }
734
735     // opens a dialog asking the user if they would like to override
736     // the returned event.
737     service.override_dialog = function(evt, params, options, action) {
738         if (!angular.isArray(evt)) evt = [evt];
739
740         egCore.audio.play('warning.circ.event_override');
741         var copy_alert = evt.filter(function(e) {
742             return e.textcode == 'COPY_ALERT_MESSAGE';
743         });
744         evt = evt.filter(function(e) {
745             return e.textcode !== 'COPY_ALERT_MESSAGE';
746         });
747
748         return $uibModal.open({
749             templateUrl: './circ/share/t_event_override_dialog',
750             backdrop: 'static',
751             controller: 
752                 ['$scope', '$uibModalInstance', 
753                 function($scope, $uibModalInstance) {
754                 $scope.events = evt;
755                 $scope.action = action;
756
757                 // Find the event, if any, that is for ITEM_ON_HOLDS_SHELF
758                 //  and grab the patron name of the owner. 
759                 $scope.holdEvent = evt.filter(function(e) {
760                     return e.textcode === 'ITEM_ON_HOLDS_SHELF'
761                 });
762
763                 if ($scope.holdEvent.length > 0) {
764                     // Ensure we have a scalar here
765                     if (angular.isArray($scope.holdEvent)) {
766                         $scope.holdEvent = $scope.holdEvent[0];
767                     }
768
769                     $scope.patronName = $scope.holdEvent.payload.patron_name;
770                     $scope.holdID = $scope.holdEvent.payload.hold_id;
771                     $scope.patronID = $scope.holdEvent.payload.patron_id;
772                 }
773
774                 $scope.copy_barcode = params.copy_barcode; // may be null
775
776                 // Implementation note: Why not use a primitive here? It
777                 // doesn't work.  See: 
778                 // http://stackoverflow.com/questions/18642371/checkbox-not-binding-to-scope-in-angularjs
779                 $scope.formdata = {
780                     clearHold : service.clearHold,
781                     nonoverridable: evt.filter(function(e){
782                         return service.nonoverridable_events.indexOf(e.textcode) > -1;}).length > 0,
783                     event_ui_data : Object.fromEntries(
784                         evt.map( e => [ e.ilsevent, {
785                             // non-overridable events will be rare, but they are skippable.  We use
786                             // the same checkbox variable to track desired skip and auto-override
787                             // selections.
788                             overridable: service.nonoverridable_events.indexOf(e.textcode) == -1,
789                             // for non-overridable events, we'll default the checkbox to any previous
790                             // choice made for the current patron, though normally the UI will be
791                             // suppressed unless some previously unencountered events are in the set
792                             checkbox: service.nonoverridable_events.indexOf(e.textcode) > -1
793                             ? (service.auto_skip_circ_events[e.textcode] == undefined
794                                 ? false
795                                 : service.auto_skip_circ_events[e.textcode]
796                             )
797                             // if a given event is overridable, said checkbox will default to any previous
798                             // choice made for the current patron, as long as there are no non-overridable
799                             // events in the set (because we'll disable the checkbox in that case and don't
800                             // want to imply that we're going to set an auto-override)
801                             : (service.auto_override_circ_events[e.textcode] == undefined
802                                 ? (
803                                     service.nonoverridable_events.indexOf(e.textcode) > -1
804                                     ? false
805                                     : service.default_auto_override.indexOf(e.textcode) > -1
806                                 )
807                                 : service.auto_override_circ_events[e.textcode]
808                             )
809                         }])
810                     ) 
811                 };
812
813                 function update_auto_override_and_skip_lists() {
814                     angular.forEach(evt, function(e){
815                         if ($scope.formdata.nonoverridable) {
816                             // the action had at least one non-overridable event, so let's only
817                             // record skip choices for those
818                             if (!$scope.formdata.event_ui_data[e.ilsevent].overridable) {
819                                 if ($scope.formdata.event_ui_data[e.ilsevent].checkbox) {
820                                     // grow the skip list
821                                     service.auto_skip_circ_events[e.textcode] = true;
822                                 } else {
823                                     // shrink the skip list
824                                     service.auto_skip_circ_events[e.textcode] = false;
825                                 }
826                             }
827                         } else {
828                             // record all auto-override choices
829                             if ($scope.formdata.event_ui_data[e.ilsevent].checkbox) {
830                                 // grow the auto-override list
831                                 service.auto_override_circ_events[e.textcode] = true;
832                             } else {
833                                 // shrink the auto-override list
834                                 service.auto_override_circ_events[e.textcode] = false;
835                             }
836                         }
837                     });
838                     // for debugging
839                     window.oils_auto_skip_circ_events = service.auto_skip_circ_events;
840                     window.oils_auto_override_circ_events = service.auto_override_circ_events;
841                 }
842
843                 $scope.ok = function() { 
844                     update_auto_override_and_skip_lists();
845                     // Handle the cancellation of the assciated hold here
846                     if ($scope.formdata.clearHold && $scope.holdID) {
847                         egCore.net.request(
848                             'open-ils.circ',
849                             'open-ils.circ.hold.cancel',
850                             egCore.auth.token(), $scope.holdID,
851                             5, // staff forced
852                             'Item checked out by other patron' // FIXME I18n
853                         );
854                     }
855                     $uibModalInstance.close();
856                 }
857
858                 $scope.skip = function($event) {
859                     update_auto_override_and_skip_lists();
860                     $uibModalInstance.dismiss();
861                     $event.preventDefault();
862                 }
863
864                 $scope.cancel = function ($event) { 
865                     window.oils_cancel_batch = true;
866                     $uibModalInstance.dismiss();
867                     $event.preventDefault();
868                 }
869             }]
870         }).result.then(
871             function() {
872                 options.override = true;
873
874                 if (copy_alert.length > 0) {
875                     return service.copy_alert_dialog(copy_alert, params, options, action);
876                 }
877
878                 if (action == 'checkin') {
879                     return service.checkin(params, options);
880                 }
881
882                 return service[action](params, options);
883             }
884         );
885     }
886
887     service.copy_not_avail_dialog = function(evt, params, options) {
888         if (!angular.isArray(evt)) evt = [evt];
889
890         var copy_alert = evt.filter(function(e) {
891             return e.textcode == 'COPY_ALERT_MESSAGE';
892         });
893         evt = evt.filter(function(e) {
894             return e.textcode !== 'COPY_ALERT_MESSAGE';
895         });
896         evt = evt[0];
897
898         return $uibModal.open({
899             templateUrl: './circ/share/t_copy_not_avail_dialog',
900             backdrop: 'static',
901             controller: 
902                        ['$scope','$uibModalInstance','copyStatus',
903                 function($scope , $uibModalInstance , copyStatus) {
904                 $scope.copyStatus = copyStatus;
905                 $scope.ok = function() {$uibModalInstance.close()}
906                 $scope.cancel = function() {$uibModalInstance.dismiss()}
907             }],
908             resolve : {
909                 copyStatus : function() {
910                     return egCore.pcrud.retrieve(
911                         'ccs', evt.payload.status());
912                 }
913             }
914         }).result.then(
915             function() {
916                 options.override = true;
917
918                 if (copy_alert.length > 0) {
919                     return service.copy_alert_dialog(copy_alert, params, options, 'checkout');
920                 }
921
922                 return service.checkout(params, options);
923             }
924         );
925     }
926
927     // Opens a dialog allowing the user to fill in the desired non-cat count.
928     // Unlike other dialogs, which kickoff circ actions internally
929     // as a result of events, this dialog does not kick off any circ
930     // actions. It just collects the count and and resolves the promise.
931     //
932     // This assumes the caller has already handled the noncat-type
933     // selection and just needs to collect the count info.
934     service.noncat_dialog = function(params, options) {
935         var noncatMax = 99; // hard-coded max
936         
937         // the caller should presumably have fetched the noncat_types via
938         // our API already, but fetch them again (from cache) to be safe.
939         return service.get_noncat_types().then(function() {
940
941             params.noncat = true;
942             var type = egCore.env.cnct.map[params.noncat_type];
943
944             return $uibModal.open({
945                 templateUrl: './circ/share/t_noncat_dialog',
946                 backdrop: 'static',
947                 controller: 
948                     ['$scope', '$uibModalInstance',
949                     function($scope, $uibModalInstance) {
950                     $scope.focusMe = true;
951                     $scope.type = type;
952                     $scope.count = 1;
953                     $scope.noncatMax = noncatMax;
954                     $scope.ok = function(count) { $uibModalInstance.close(count) }
955                     $scope.cancel = function ($event) { 
956                         $uibModalInstance.dismiss() 
957                         $event.preventDefault();
958                     }
959                 }],
960             }).result.then(
961                 function(count) {
962                     if (count && count > 0 && count <= noncatMax) { 
963                         // NOTE: in Chrome, form validation ensure a valid number
964                         params.noncat_count = count;
965                         return $q.when(params);
966                     } else {
967                         return $q.reject();
968                     }
969                 }
970             );
971         });
972     }
973
974     // Opens a dialog allowing the user to fill in pre-cat copy info.
975     service.precat_dialog = function(params, options) {
976
977         return $uibModal.open({
978             templateUrl: './circ/share/t_precat_dialog',
979             backdrop: 'static',
980             controller: 
981                 ['$scope', '$uibModalInstance', 'circMods', 'has_precat_perm',
982                 function($scope, $uibModalInstance, circMods, has_precat_perm) {
983                 $scope.focusMe = true;
984                 $scope.precatArgs = {
985                     copy_barcode : params.copy_barcode
986                 };
987
988                 $scope.can_create_precats = has_precat_perm;
989                 $scope.circModifiers = circMods;
990                 $scope.ok = function(args) { $uibModalInstance.close(args) }
991                 $scope.cancel = function () { $uibModalInstance.dismiss() }
992
993                 // use this function as a keydown handler on form
994                 // elements that should not submit the form on enter.
995                 $scope.preventSubmit = function($event) {
996                     if ($event.keyCode == 13)
997                         $event.preventDefault();
998                 }
999             }],
1000             resolve : {
1001                 circMods : function() { return service.get_circ_mods(); },
1002                 has_precat_perm : function(){ return egCore.perm.hasPermHere('CREATE_PRECAT'); }
1003             }
1004         }).result.then(
1005             function(args) {
1006                 if (!args || !args.dummy_title) return $q.reject();
1007                 if(args.circ_modifier == "") args.circ_modifier = null;
1008                 angular.forEach(args, function(val, key) {params[key] = val});
1009                 params.precat = true;
1010                 return service.checkout(params, options);
1011             }
1012         );
1013     }
1014
1015     // find the open transit for the given copy barcode; flesh the org
1016     // units locally.
1017     service.find_copy_transit = function(evt, params, options) {
1018         if (angular.isArray(evt)) evt = evt[0];
1019
1020         // NOTE: evt.payload.transit may exist, but it's not necessarily
1021         // the transit we want, since a transit close + open in the API
1022         // returns the closed transit.
1023
1024          return egCore.pcrud.search('atc',
1025             {   dest_recv_time : null, cancel_time : null},
1026             {   flesh : 1, 
1027                 flesh_fields : {atc : ['target_copy']},
1028                 join : {
1029                     acp : {
1030                         filter : {
1031                             barcode : params.copy_barcode,
1032                             deleted : 'f'
1033                         }
1034                     }
1035                 },
1036                 limit : 1,
1037                 order_by : {atc : 'source_send_time desc'}, 
1038             }, {authoritative : true}
1039         ).then(function(transit) {
1040             transit.source(egCore.org.get(transit.source()));
1041             transit.dest(egCore.org.get(transit.dest()));
1042             return transit;
1043         });
1044     }
1045
1046     service.copy_in_transit_dialog = function(evt, params, options) {
1047         if (angular.isArray(evt)) evt = evt[0];
1048         return $uibModal.open({
1049             templateUrl: './circ/share/t_copy_in_transit_dialog',
1050             backdrop: 'static',
1051             controller: 
1052                        ['$scope','$uibModalInstance','transit',
1053                 function($scope , $uibModalInstance , transit) {
1054                 $scope.transit = transit;
1055                 $scope.ok = function() { $uibModalInstance.close(transit) }
1056                 $scope.cancel = function() { $uibModalInstance.dismiss() }
1057             }],
1058             resolve : {
1059                 // fetch the conflicting open transit w/ fleshed copy
1060                 transit : function() {
1061                     return service.find_copy_transit(evt, params, options);
1062                 }
1063             }
1064         }).result.then(
1065             function(transit) {
1066                 // user chose to abort the transit then checkout
1067                 return service.abort_transit(transit.id())
1068                 .then(function() {
1069                     return service.checkout(params, options);
1070                 });
1071             }
1072         );
1073     }
1074
1075     service.abort_transit = function(transit_id) {
1076         return egCore.net.request(
1077             'open-ils.circ',
1078             'open-ils.circ.transit.abort',
1079             egCore.auth.token(), {transitid : transit_id}
1080         ).then(function(resp) {
1081             if (evt = egCore.evt.parse(resp)) {
1082                 alert(evt);
1083                 return $q.reject();
1084             }
1085             return $q.when();
1086         });
1087     }
1088
1089     service.last_copy_circ = function(copy_id) {
1090         return egCore.pcrud.search('circ', 
1091             {target_copy : copy_id},
1092             {order_by : {circ : 'xact_start desc' }, limit : 1}
1093         );
1094     }
1095
1096     service.circ_exists_dialog = function(evt, params, options) {
1097         if (angular.isArray(evt)) evt = evt[0];
1098
1099         if (!evt.payload.old_circ) {
1100             return egCore.net.request(
1101                 'open-ils.search',
1102                 'open-ils.search.asset.copy.fleshed2.find_by_barcode',
1103                 params.copy_barcode
1104             ).then(function(resp){
1105                 console.log(resp);
1106                 if (egCore.evt.parse(resp)) {
1107                     console.error(egCore.evt.parse(resp));
1108                 } else {
1109                     return egCore.net.request(
1110                          'open-ils.circ',
1111                          'open-ils.circ.copy_checkout_history.retrieve',
1112                          egCore.auth.token(), resp.id(), 1
1113                     ).then( function (circs) {
1114                         evt.payload.old_circ = circs[0];
1115                         return service.circ_exists_dialog_impl( evt, params, options );
1116                     });
1117                 }
1118             });
1119         } else {
1120             return service.circ_exists_dialog_impl( evt, params, options );
1121         }
1122     },
1123
1124     service.circ_exists_dialog_impl = function (evt, params, options) {
1125
1126         var openCirc = evt.payload.old_circ;
1127         var sameUser = openCirc.usr() == params.patron_id;
1128         
1129         return $uibModal.open({
1130             templateUrl: './circ/share/t_circ_exists_dialog',
1131             backdrop: 'static',
1132             controller: 
1133                        ['$scope','$uibModalInstance',
1134                 function($scope , $uibModalInstance) {
1135                 $scope.args = {forgive_fines : false};
1136                 $scope.circDate = openCirc.xact_start();
1137                 $scope.sameUser = sameUser;
1138                 $scope.ok = function() { $uibModalInstance.close($scope.args) }
1139                 $scope.cancel = function($event) { 
1140                     $uibModalInstance.dismiss();
1141                     $event.preventDefault(); // form, avoid calling ok();
1142                 }
1143             }]
1144         }).result.then(
1145             function(args) {
1146                 if (sameUser) {
1147                     params.void_overdues = args.forgive_fines;
1148                     options.sameCopyCheckout = true;
1149                     return service.renew(params, options);
1150                 }
1151
1152                 return service.checkin({
1153                     barcode : params.copy_barcode,
1154                     noop : true,
1155                     void_overdues : args.forgive_fines
1156                 }).then(function(checkin_resp) {
1157                     if (checkin_resp.evt[0].textcode == 'SUCCESS') {
1158                         return service.checkout(params, options);
1159                     } else {
1160                         alert(egCore.evt.parse(checkin_resp.evt[0]));
1161                         return $q.reject();
1162                     }
1163                 });
1164             }
1165         );
1166     }
1167
1168     service.batch_backdate = function(circ_ids, backdate) {
1169         return egCore.net.request(
1170             'open-ils.circ',
1171             'open-ils.circ.post_checkin_backdate.batch',
1172             egCore.auth.token(), circ_ids, backdate);
1173     }
1174
1175     service.backdate_dialog = function(circ_ids) {
1176         return $uibModal.open({
1177             templateUrl: './circ/share/t_backdate_dialog',
1178             backdrop: 'static',
1179             controller: 
1180                        ['$scope','$uibModalInstance',
1181                 function($scope , $uibModalInstance) {
1182
1183                 var today = new Date();
1184                 $scope.dialog = {
1185                     num_circs : circ_ids.length,
1186                     num_processed : 0,
1187                     backdate : today
1188                 }
1189
1190                 $scope.$watch('dialog.backdate', function(newval) {
1191                     if (newval && newval > today) 
1192                         $scope.dialog.backdate = today;
1193                 });
1194
1195
1196                 $scope.cancel = function() { 
1197                     $uibModalInstance.dismiss();
1198                 }
1199
1200                 $scope.ok = function() { 
1201
1202                     var bd = $scope.dialog.backdate.toISOString().replace(/T.*/,'');
1203                     service.batch_backdate(circ_ids, bd)
1204                     .then(
1205                         function() { // on complete
1206                             $uibModalInstance.close({backdate : bd});
1207                         },
1208                         null,
1209                         function(resp) { // on response
1210                             console.debug('backdate returned ' + resp);
1211                             if (resp == '1') {
1212                                 $scope.num_processed++;
1213                             } else {
1214                                 console.error(egCore.evt.parse(resp));
1215                             }
1216                         }
1217                     );
1218                 }
1219             }]
1220         }).result;
1221     }
1222
1223     service.mark_claims_returned = function(barcode, date, override) {
1224
1225         var method = 'open-ils.circ.circulation.set_claims_returned';
1226         if (override) method += '.override';
1227
1228         console.debug('claims returned ' + method);
1229
1230         return egCore.net.request(
1231             'open-ils.circ', method, egCore.auth.token(),
1232             {barcode : barcode, backdate : date})
1233
1234         .then(function(resp) {
1235
1236             if (resp == 1) { // success
1237                 console.debug('claims returned succeeded for ' + barcode);
1238                 return barcode;
1239
1240             } else if (evt = egCore.evt.parse(resp)) {
1241                 console.debug('claims returned failed: ' + evt.toString());
1242
1243                 if (evt.textcode == 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT') {
1244                     // TODO check perms before offering override option?
1245
1246                     if (override) return;// just to be safe
1247
1248                     return egConfirmDialog.open(
1249                         egCore.strings.TOO_MANY_CLAIMS_RETURNED, '', {}
1250                     ).result.then(function() {
1251                         return service.mark_claims_returned(barcode, date, true);
1252                     });
1253                 }
1254
1255                 if (evt.textcode == 'PERM_FAILURE') {
1256                     console.error('claims returned permission denied')
1257                     // TODO: auth override dialog?
1258                 }
1259             }
1260         });
1261     }
1262
1263     service.mark_claims_returned_dialog = function(copy_barcodes) {
1264         if (!copy_barcodes.length) return;
1265
1266         return $uibModal.open({
1267             templateUrl: './circ/share/t_mark_claims_returned_dialog',
1268             backdrop: 'static',
1269             controller: 
1270                        ['$scope','$uibModalInstance',
1271                 function($scope , $uibModalInstance) {
1272
1273                 var today = new Date();
1274                 $scope.args = {
1275                     barcodes : copy_barcodes,
1276                     date : today
1277                 };
1278
1279                 $scope.$watch('args.date', function(newval) {
1280                     if (newval && newval > today) 
1281                         $scope.args.backdate = today;
1282                 });
1283
1284                 $scope.cancel = function() {$uibModalInstance.dismiss()}
1285                 $scope.ok = function() { 
1286
1287                     var date = $scope.args.date.toISOString().replace(/T.*/,'');
1288
1289                     var deferred = $q.defer();
1290
1291                     // serialize the action on each barcode so that the 
1292                     // caller will never see multiple alerts at the same time.
1293                     function mark_one() {
1294                         var bc = copy_barcodes.pop();
1295                         if (!bc) {
1296                             deferred.resolve();
1297                             $uibModalInstance.close();
1298                             return;
1299                         }
1300
1301                         // finally -> continue even when one fails
1302                         service.mark_claims_returned(bc, date)
1303                         .finally(function(barcode) {
1304                             if (barcode) deferred.notify(barcode);
1305                             mark_one();
1306                         });
1307                     }
1308                     mark_one(); // kick it off
1309                     return deferred.promise;
1310                 }
1311             }]
1312         }).result;
1313     }
1314
1315     // serially checks in each barcode with claims_never_checked_out set
1316     // returns promise, notified on each barcode, resolved after all
1317     // checkins are complete.
1318     service.mark_claims_never_checked_out = function(barcodes) {
1319         if (!barcodes.length) return;
1320
1321         var deferred = $q.defer();
1322         egConfirmDialog.open(
1323             egCore.strings.MARK_NEVER_CHECKED_OUT, '', {barcodes : barcodes}
1324
1325         ).result.then(function() {
1326             function mark_one() {
1327                 var bc = barcodes.pop();
1328
1329                 if (!bc) { // all done
1330                     deferred.resolve();
1331                     return;
1332                 }
1333
1334                 service.checkin(
1335                     {claims_never_checked_out : true, copy_barcode : bc})
1336                 .finally(function() { 
1337                     deferred.notify(bc);
1338                     mark_one();
1339                 })
1340             }
1341             mark_one();
1342         });
1343
1344         return deferred.promise;
1345     }
1346
1347     service.mark_damaged = function(params) {
1348         if (!params) return $q.when();
1349         return $uibModal.open({
1350             backdrop: 'static',
1351             templateUrl: './circ/share/t_mark_damaged',
1352             controller:
1353                 ['$scope', '$uibModalInstance', 'egCore', 'egBilling', 'egItem',
1354                 function($scope, $uibModalInstance, egCore, egBilling, egItem) {
1355                     var doRefresh = params.refresh;
1356                     
1357                     $scope.showBill = params.charge != null && params.circ;
1358                     $scope.billArgs = {charge: params.charge};
1359                     $scope.mode = 'charge';
1360                     $scope.barcode = params.barcode;
1361                     if (params.circ) {
1362                         $scope.circ = params.circ;
1363                         $scope.circ_checkin_time = params.circ.checkin_time();
1364                         $scope.circ_patron_name = params.circ.usr().family_name() + ", "
1365                             + params.circ.usr().first_given_name() + " "
1366                             + params.circ.usr().second_given_name();
1367                     }
1368                     egBilling.fetchBillingTypes().then(function(res) {
1369                         $scope.billingTypes = res;
1370                     });
1371
1372                     $scope.btnChargeFees = function() {
1373                         $scope.mode = 'charge';
1374                         $scope.billArgs.charge = params.charge;
1375                     }
1376                     $scope.btnWaiveFees = function() {
1377                         $scope.mode = 'waive';
1378                         $scope.billArgs.charge = 0;
1379                     }
1380
1381                     $scope.cancel = function ($event) { 
1382                         $uibModalInstance.dismiss();
1383                     }
1384                     $scope.ok = function() {
1385                         handle_mark_item_damaged();
1386                     }
1387
1388                     var handle_mark_item_damaged = function() {
1389                         var applyFines;
1390                         if ($scope.showBill)
1391                             applyFines = $scope.billArgs.charge ? 'apply' : 'noapply';
1392
1393                         egCore.net.request(
1394                             'open-ils.circ',
1395                             'open-ils.circ.mark_item_damaged',
1396                             egCore.auth.token(), params.id, {
1397                                 apply_fines: applyFines,
1398                                 override_amount: $scope.billArgs.charge,
1399                                 override_btype: $scope.billArgs.type,
1400                                 override_note: $scope.billArgs.note,
1401                                 handle_checkin: !applyFines
1402                         }).then(function(resp) {
1403                             if (evt = egCore.evt.parse(resp)) {
1404                                 doRefresh = false;
1405                                 console.debug("mark damaged more information required. Pushing back.");
1406                                 service.mark_damaged({
1407                                     id: params.id,
1408                                     barcode: params.barcode,
1409                                     charge: evt.payload.charge,
1410                                     circ: evt.payload.circ,
1411                                     refresh: params.refresh
1412                                 });
1413                                 console.error('mark damaged failed: ' + evt);
1414                             }
1415                         }).then(function() {
1416                             if (doRefresh) egItem.add_barcode_to_list(params.barcode);
1417                         });
1418                         $uibModalInstance.close();
1419                     }
1420                 }]
1421         }).result;
1422     }
1423
1424     service.handle_mark_item_event = function(copy, status, args, event) {
1425         var dlogTitle, dlogMessage;
1426         switch (event.textcode) {
1427         case 'ITEM_TO_MARK_CHECKED_OUT':
1428             if (status.id() === 4) {
1429                 // checked out items shouldn't be marked missing
1430                 console.error(
1431                     'Mark item ' + status.name() + ' for ' +
1432                     copy.barcode + ' failed: ' + event
1433                 );
1434                 return service.exit_alert(
1435                     egCore.strings.MARK_MISSING_FAILURE_CHECKED_OUT,
1436                     {barcode : copy.barcode});
1437             }
1438             dlogTitle = egCore.strings.MARK_ITEM_CHECKED_OUT;
1439             dlogMessage = egCore.strings.MARK_ITEM_CHECKIN_CONTINUE;
1440             args.handle_checkin = 1;
1441             break;
1442         case 'ITEM_TO_MARK_IN_TRANSIT':
1443             dlogTitle = egCore.strings.MARK_ITEM_IN_TRANSIT;
1444             dlogMessage = egCore.strings.MARK_ITEM_ABORT_CONTINUE;
1445             args.handle_transit = 1;
1446             break;
1447         case 'ITEM_TO_MARK_LAST_HOLD_COPY':
1448             dlogTitle = egCore.strings.MARK_ITEM_LAST_HOLD_COPY;
1449             dlogMessage = egCore.strings.MARK_ITEM_CONTINUE;
1450             args.handle_last_hold_copy = 1;
1451             break;
1452         case 'COPY_DELETE_WARNING':
1453             dlogTitle = egCore.strings.MARK_ITEM_RESTRICT_DELETE;
1454             dlogMessage = egCore.strings.MARK_ITEM_CONTINUE;
1455             args.handle_copy_delete_warning = 1;
1456             break;
1457         case 'PERM_FAILURE':
1458             console.error('Mark item ' + status.name() + ' for ' + copy.barcode + ' failed: ' +
1459                           event);
1460             return service.exit_alert(egCore.strings.PERMISSION_DENIED,
1461                                       {permission : event.ilsperm});
1462             break;
1463         default:
1464             console.error('Mark item ' + status.name() + ' for ' + copy.barcode + ' failed: ' +
1465                           event);
1466             return service.exit_alert(egCore.strings.MARK_ITEM_FAILURE,
1467                                       {status : status.name(), barcode : copy.barcode,
1468                                        textcode : event.textcode});
1469             break;
1470         }
1471         return egConfirmDialog.open(
1472             dlogTitle, dlogMessage,
1473             {
1474                 barcode : copy.barcode,
1475                 status : status.name(),
1476                 ok : function () {},
1477                 cancel : function () {}
1478             }
1479         ).result.then(function() {
1480             return service.mark_item(copy, status, args);
1481         });
1482     }
1483
1484     service.mark_item = function(copy, markstatus, args) {
1485         if (!copy) return $q.when();
1486
1487         // If any new back end mark_item calls are added, also add
1488         // them here to use them from the staff client.
1489         // TODO: I didn't find any JS constants for copy status.
1490         var req;
1491         switch (markstatus.id()) {
1492         case 2:
1493             // Not implemented in the staff client, yet.
1494             // req = "open-ils.circ.mark_item_bindery";
1495             break;
1496         case 4:
1497             req = "open-ils.circ.mark_item_missing";
1498             break;
1499         case 9:
1500             // Not implemented in the staff client, yet.
1501             // req = "open-ils.circ.mark_item_on_order";
1502             break;
1503         case 10:
1504             // Not implemented in the staff client, yet.
1505             // req = "open-ils.circ.mark_item_ill";
1506             break;
1507         case 11:
1508             // Not implemented in the staff client, yet.
1509             // req = "open-ils.circ.mark_item_cataloging";
1510             break;
1511         case 12:
1512             // Not implemented in the staff client, yet.
1513             // req = "open-ils.circ.mark_item_reserves";
1514             break;
1515         case 13:
1516             req = "open-ils.circ.mark_item_discard";
1517             break;
1518         case 14:
1519             // Damaged is for handling of events. It's main handler is elsewhere.
1520             req = "open-ils.circ.mark_item_damaged";
1521             break;
1522         }
1523
1524         return egCore.net.request(
1525             'open-ils.circ',
1526             req,
1527             egCore.auth.token(),
1528             copy.id,
1529             args
1530         ).then(function(resp) {
1531             if (evt = egCore.evt.parse(resp)) {
1532                 return service.handle_mark_item_event(copy, markstatus, args, evt);
1533             }
1534         });
1535     }
1536
1537     service.mark_discard = function(copies) {
1538         return egConfirmDialog.open(
1539             egCore.strings.MARK_DISCARD_CONFIRM, '',
1540             {
1541                 num_items : copies.length,
1542                 ok : function() {},
1543                 cancel : function() {}
1544             }
1545         ).result.then(function() {
1546             return egCore.pcrud.retrieve('ccs', 13)
1547                 .then(function(resp) {
1548                     var promises = [];
1549                     angular.forEach(copies, function(copy) {
1550                         promises.push(service.mark_item(copy, resp, {}))
1551                     });
1552                     return $q.all(promises);
1553                 });
1554         });
1555     }
1556
1557     service.mark_missing = function(copies) {
1558         return egConfirmDialog.open(
1559             egCore.strings.MARK_MISSING_CONFIRM, '',
1560             {
1561                 num_items : copies.length,
1562                 ok : function() {},
1563                 cancel : function() {}
1564             }
1565         ).result.then(function() {
1566             return egCore.pcrud.retrieve('ccs', 4)
1567                 .then(function(resp) {
1568                     var modified = [];
1569                     var promise = $q.when();
1570                     angular.forEach(copies, function(copy) {
1571                         promise = promise.then(function() {
1572                             return service.mark_item(
1573                                 copy, resp, {}
1574                             ).then(function() {
1575                                 modified.push(copy.barcode);
1576                             }).catch(function(){});
1577                         });
1578                     });
1579                     promise = promise.then(function() {
1580                         if (!modified.length) return $q.reject();
1581                         return modified;
1582                     });
1583                     return promise;
1584                 });
1585         });
1586     }
1587
1588
1589
1590     // Mark circulations as lost via copy barcode.  As each item is 
1591     // processed, the returned promise is notified of the barcode.
1592     // No confirmation dialog is presented.
1593     service.mark_lost = function(copy_barcodes) {
1594         var deferred = $q.defer();
1595         var promises = [];
1596
1597         angular.forEach(copy_barcodes, function(barcode) {
1598             promises.push(
1599                 egCore.net.request(
1600                     'open-ils.circ',
1601                     'open-ils.circ.circulation.set_lost',
1602                     egCore.auth.token(), {barcode : barcode}
1603                 ).then(function(resp) {
1604                     if (evt = egCore.evt.parse(resp)) {
1605                         console.error("Mark lost failed: " + evt.toString());
1606                         return;
1607                     }
1608                     // inform the caller as each item is processed
1609                     deferred.notify(barcode);
1610                 })
1611             );
1612         });
1613
1614         $q.all(promises).then(function() {deferred.resolve()});
1615         return deferred.promise;
1616     }
1617
1618     service.abort_transits = function(transit_ids) {
1619         return egConfirmDialog.open(
1620             egCore.strings.ABORT_TRANSIT_CONFIRM, '',
1621             {   num_transits : transit_ids.length,
1622                 ok : function() {},
1623                 cancel : function() {}
1624             }
1625
1626         ).result.then(function() {
1627             var promises = [];
1628             angular.forEach(transit_ids, function(transit_id) {
1629                 promises.push(
1630                     egCore.net.request(
1631                         'open-ils.circ',
1632                         'open-ils.circ.transit.abort',
1633                         egCore.auth.token(), {transitid : transit_id}
1634                     ).then(function(resp) {
1635                         if (evt = egCore.evt.parse(resp)) {
1636                             console.error('abort transit failed: ' + evt);
1637                         }
1638                     })
1639                 );
1640             });
1641
1642             return $q.all(promises);
1643         });
1644     }
1645
1646     service.add_copy_alerts = function(item_ids) {
1647         return egAddCopyAlertDialog.open({
1648             copy_ids : item_ids,
1649             ok : function() { },
1650             cancel : function() {}
1651         }).result.then(function() { });
1652     }
1653
1654     service.manage_copy_alerts = function(item_ids) {
1655         return egCopyAlertEditorDialog.open({
1656             copy_id : item_ids[0],
1657             ok : function() { },
1658             cancel : function() {}
1659         }).result.then(function() { });
1660     }
1661
1662     // alert when copy location alert_message is set.
1663     // This does not affect processing, it only produces a click-through
1664     service.handle_checkin_loc_alert = function(evt, params, options) {
1665         if (angular.isArray(evt)) evt = evt[0];
1666
1667         var copy = evt && evt.payload ? evt.payload.copy : null;
1668
1669         if (copy && !options.suppress_popups
1670             && copy.location().checkin_alert() == 't') {
1671
1672             return egAlertDialog.open(
1673                 egCore.strings.LOCATION_ALERT_MSG, {copy : copy}).result;
1674         }
1675
1676         return $q.when();
1677     }
1678
1679     service.handle_checkin_resp = function(evt, params, options) {
1680         if (!angular.isArray(evt)) evt = [evt];
1681
1682         var final_resp = {evt : evt, params : params, options : options};
1683
1684         var copy, hold, transit;
1685         if (evt[0].payload) {
1686             copy = evt[0].payload.copy;
1687             hold = evt[0].payload.hold;
1688             transit = evt[0].payload.transit;
1689         }
1690
1691         // track the barcode regardless of whether it's valid
1692         angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
1693
1694         angular.forEach(evt, function(e){ console.debug('checkin event ' + e.textcode); });
1695
1696         if (evt.filter(function(e){return service.checkin_overridable_events.indexOf(e.textcode) > -1;}).length > 0)
1697             return service.handle_overridable_checkin_event(evt, params, options);
1698
1699         switch (evt[0].textcode) {
1700
1701             case 'SUCCESS':
1702             case 'NO_CHANGE':
1703
1704                 switch(Number(copy.status().id())) {
1705
1706                     case 0: /* AVAILABLE */                                        
1707                     case 4: /* MISSING */                                          
1708                     case 7: /* RESHELVING */ 
1709
1710                         egCore.audio.play('success.checkin');
1711
1712                         // see if the copy location requires an alert
1713                         return service.handle_checkin_loc_alert(evt, params, options)
1714                         .then(function() {return final_resp});
1715
1716                     case 8: /* ON HOLDS SHELF */
1717                         egCore.audio.play('info.checkin.holds_shelf');
1718                         
1719                         if (hold) {
1720
1721                             if (hold.pickup_lib() == egCore.auth.user().ws_ou()) {
1722                                 // inform user if the item is on the local holds shelf
1723                             
1724                                 evt[0].route_to = egCore.strings.ROUTE_TO_HOLDS_SHELF;
1725                                 return service.route_dialog(
1726                                     './circ/share/t_hold_shelf_dialog', 
1727                                     evt[0], params, options
1728                                 ).then(function() { return final_resp });
1729
1730                             } else {
1731                                 // normally, if the hold was on the shelf at a 
1732                                 // different location, it would be put into 
1733                                 // transit, resulting in a ROUTE_ITEM event.
1734                                 egCore.audio.play('warning.checkin.wrong_shelf');
1735                                 return $q.when(final_resp);
1736                             }
1737                         } else {
1738
1739                             console.error('checkin: item on holds shelf, '
1740                                 + 'but hold info not returned from checkin');
1741                             return $q.when(final_resp);
1742                         }
1743
1744                     case 11: /* CATALOGING */
1745                         egCore.audio.play('info.checkin.cataloging');
1746                         evt[0].route_to = egCore.strings.ROUTE_TO_CATALOGING;
1747                         if (options.no_precat_alert || options.suppress_popups)
1748                             return $q.when(final_resp);
1749                         return egAlertDialog.open(
1750                             egCore.strings.PRECAT_CHECKIN_MSG, params)
1751                             .result.then(function() {return final_resp});
1752
1753
1754                     case 15: /* ON_RESERVATION_SHELF */
1755                         egCore.audio.play('info.checkin.reservation');
1756                         // TODO: show booking reservation dialog
1757                         return $q.when(final_resp);
1758
1759                     default:
1760                         egCore.audio.play('success.checkin');
1761                         console.debug('Unusual checkin copy status (may have been set via copy alert): '
1762                             + copy.status().id() + ' : ' + copy.status().name());
1763                         return $q.when(final_resp);
1764                 }
1765                 
1766             case 'ROUTE_ITEM':
1767                 return service.route_dialog(
1768                     './circ/share/t_transit_dialog', 
1769                     evt[0], params, options
1770                 ).then(function(data) {
1771                     if (transit && data.transit && transit.dest().id() != data.transit.dest().id())
1772                         final_resp.evt[0].route_to = data.transit.dest().shortname();
1773                     return final_resp;
1774                 });
1775
1776             case 'ASSET_COPY_NOT_FOUND':
1777                 egCore.audio.play('error.checkin.not_found');
1778                 if (options.suppress_popups) return $q.when(final_resp);
1779                 return egAlertDialog.open(
1780                     egCore.strings.UNCAT_ALERT_DIALOG, params)
1781                     .result.then(function() {return final_resp});
1782
1783             case 'ITEM_NOT_CATALOGED':
1784                 egCore.audio.play('error.checkin.not_cataloged');
1785                 evt[0].route_to = egCore.strings.ROUTE_TO_CATALOGING;
1786                 if (options.no_precat_alert || options.suppress_popups)
1787                     return $q.when(final_resp);
1788                 return egAlertDialog.open(
1789                     egCore.strings.PRECAT_CHECKIN_MSG, params)
1790                     .result.then(function() {return final_resp});
1791
1792             case 'HOLD_CAPTURE_DELAYED':
1793                 return service.hold_capture_delay_dialog(
1794                     evt[0], params, options, 'checkin');
1795
1796             default:
1797                 egCore.audio.play('error.checkin.unknown');
1798                 console.warn('unhandled checkin response : ' + evt[0].textcode);
1799                 return $q.when(final_resp);
1800         }
1801     }
1802
1803     // collect transit, addresses, and hold info that's not already
1804     // included in responses.
1805     service.collect_route_data = function(tmpl, evt, params, options) {
1806         if (angular.isArray(evt)) evt = evt[0];
1807         var promises = [];
1808         var data = {};
1809         var addr_deferred = $q.defer();
1810         // associates org units with the address they're needed for
1811         var addr_orgs = {};
1812         promises.push(addr_deferred.promise);
1813         
1814         if (evt.org && !tmpl.match(/hold_shelf/)) {
1815             addr_orgs['address'] = evt.org; 
1816         }
1817                 
1818                 if(evt.payload.transit){
1819             addr_orgs['source_address'] = evt.payload.transit.source().id();                    
1820                 }
1821
1822         if(Object.keys(addr_orgs).length){
1823             promises.push(
1824                 service.cache_org_addr(Object.values(addr_orgs),'holds_address')
1825                 .then(function(){
1826                     // promise to assign all of the addresses we need
1827                     var addr_promises = [];
1828                     Object.keys(addr_orgs).forEach(function(key){
1829                         addr_promises.push(
1830                             service.get_org_addr(addr_orgs[key], 'holds_address')
1831                             .then(function(addr) { 
1832                                 // assign address to field in data
1833                                 data[key] = addr; 
1834                             })
1835                         )
1836                     });
1837                     $q.all(addr_promises).then(addr_deferred.resolve());
1838             }));
1839         }
1840         else{
1841             // no addresses are needed so continue
1842             addr_deferred.resolve();
1843         }
1844         
1845         if (evt.payload.hold) {
1846             promises.push(
1847                 egCore.pcrud.retrieve('au', 
1848                     evt.payload.hold.usr(), {
1849                         flesh : 1,
1850                         flesh_fields : {'au' : ['card', 'profile']}
1851                     }
1852                 ).then(function(patron) {data.patron = patron})
1853             );
1854         }
1855
1856
1857         if (!tmpl.match(/hold_shelf/)) {
1858             var courier_deferred = $q.defer();
1859             promises.push(courier_deferred.promise);
1860             promises.push(
1861                 service.find_copy_transit(evt, params, options)
1862                 .then(function(trans) {
1863                     data.transit = trans;
1864                     egCore.org.settings('lib.courier_code', trans.dest().id())
1865                     .then(function(s) {
1866                         data.dest_courier_code = s['lib.courier_code'];
1867                         courier_deferred.resolve();
1868                     });
1869                 })
1870             );
1871         }
1872
1873         return $q.all(promises).then(function() { return data });
1874     }
1875
1876     service.route_dialog = function(tmpl, evt, params, options) {
1877         if (angular.isArray(evt)) evt = evt[0];
1878
1879         return service.collect_route_data(tmpl, evt, params, options)
1880         .then(function(data) {
1881
1882             var template = data.transit ?
1883                 (data.patron ? 'hold_transit_slip' : 'transit_slip') :
1884                 'hold_shelf_slip';
1885             if (service.never_auto_print[template]) {
1886                 // do not show the dialog or print if the
1887                 // disabled automatic print attempt type list includes
1888                 // the specified template
1889                 return data;
1890             }
1891
1892             // All actions flow from the print data
1893
1894             var print_context = {
1895                 copy : egCore.idl.toHash(evt.payload.copy),
1896                 title : evt.title,
1897                 author : evt.author,
1898                 call_number : egCore.idl.toHash(evt.payload.volume)
1899             };
1900
1901             var acn = print_context.call_number; // fix up pre/suffixes
1902             if (acn.prefix == -1) acn.prefix = "";
1903             if (acn.suffix == -1) acn.suffix = "";
1904
1905             if (data.transit) {
1906                 // route_dialog includes the "route to holds shelf" 
1907                 // dialog, which has no transit
1908                 print_context.transit = egCore.idl.toHash(data.transit);
1909                 print_context.dest_courier_code = data.dest_courier_code;
1910                 if (data.address) {
1911                     print_context.dest_address = egCore.idl.toHash(data.address);
1912                 }
1913                 if (data.source_address) {
1914                     print_context.source_address = egCore.idl.toHash(data.source_address);
1915                 }
1916                 print_context.dest_location =
1917                     egCore.idl.toHash(egCore.org.get(data.transit.dest()));
1918                 print_context.source_location =
1919                     egCore.idl.toHash(egCore.org.get(data.transit.source()));
1920                 print_context.copy.status = egCore.idl.toHash(print_context.copy.status);
1921             }
1922
1923             if (data.patron) {
1924                 print_context.hold = egCore.idl.toHash(evt.payload.hold);
1925                 var notes = print_context.hold.notes;
1926                 if(notes.length > 0){
1927                     print_context.hold_notes = [];
1928                     angular.forEach(notes, function(n){
1929                         print_context.hold_notes.push(n);
1930                     });
1931                 }
1932                 print_context.patron = egCore.idl.toHash(data.patron);
1933             }
1934
1935             var sound = 'info.checkin.transit';
1936             if (evt.payload.hold) sound += '.hold';
1937             egCore.audio.play(sound);
1938
1939             function print_transit(template) {
1940                 return egCore.print.print({
1941                     context : 'default', 
1942                     template : template, 
1943                     scope : print_context
1944                 }).then(function() { return data });
1945             }
1946
1947             // when auto-print is on, skip the dialog and go straight
1948             // to printing.
1949             if (options.auto_print_holds_transits || options.suppress_popups) 
1950                 return print_transit(template);
1951
1952             return $uibModal.open({
1953                 templateUrl: tmpl,
1954                 backdrop: 'static',
1955                 controller: [
1956                             '$scope','$uibModalInstance',
1957                     function($scope , $uibModalInstance) {
1958
1959                     $scope.today = new Date();
1960
1961                     // copy the print scope into the dialog scope
1962                     angular.forEach(print_context, function(val, key) {
1963                         $scope[key] = val;
1964                     });
1965
1966                     $scope.ok = function() {$uibModalInstance.close()}
1967
1968                     $scope.print = function() { 
1969                         $uibModalInstance.close();
1970                         print_transit(template);
1971                     }
1972                 }]
1973
1974             }).result.then(function() { return data });
1975         });
1976     }
1977
1978     // action == what action to take if the user confirms the alert
1979     service.copy_alert_dialog = function(evt, params, options, action) {
1980         egCore.audio.play('warning.circ.item_alert');
1981         if (angular.isArray(evt)) evt = evt[0];
1982         if (!angular.isArray(evt.payload)) {
1983             return egConfirmDialog.open(
1984                 egCore.strings.COPY_ALERT_MSG_DIALOG_TITLE, 
1985                 evt.payload,  // payload == alert message text
1986                 {   copy_barcode : params.copy_barcode,
1987                     ok : function() {},
1988                     cancel : function() {}
1989                 }
1990             ).result.then(function() {
1991                 options.override = true;
1992                 return service[action](params, options);
1993             });
1994         } else { // we got a list of copy alert objects ...
1995             return egCopyAlertManagerDialog.open({
1996                 alerts : evt.payload,
1997                 mode : action,
1998                 ok : function(the_next_status) {
1999                         if (the_next_status !== null) {
2000                             params.next_copy_status = [ the_next_status ];
2001                             params.capture = 'nocapture';
2002                         }
2003                      },
2004                 cancel : function() {}
2005             }).result.then(function() {
2006                 options.override = true;
2007                 return service[action](params, options);
2008             });
2009         }
2010     }
2011
2012     // action == what action to take if the user confirms the alert
2013     service.hold_capture_delay_dialog = function(evt, params, options, action) {
2014         if (angular.isArray(evt)) evt = evt[0];
2015         return $uibModal.open({
2016             templateUrl: './circ/checkin/t_hold_verify',
2017             backdrop: 'static',
2018             controller:
2019                        ['$scope','$uibModalInstance','params',
2020                 function($scope , $uibModalInstance , params) {
2021                 $scope.copy_barcode = params.copy_barcode;
2022                 $scope.capture = function() {
2023                     params.capture = 'capture';
2024                     $uibModalInstance.close();
2025                 };
2026                 $scope.nocapture = function() {
2027                     params.capture = 'nocapture';
2028                     $uibModalInstance.close();
2029                 };
2030                 $scope.cancel = function() { $uibModalInstance.dismiss(); };
2031             }],
2032             resolve : {
2033                 params : function() {
2034                     return params;
2035                 }
2036             }
2037         }).result.then(
2038             function(r) {
2039                 return service[action](params, options);
2040             }
2041         );
2042     }
2043
2044     // check the barcode.  If it's no good, show the warning dialog
2045     // Resolves on success, rejected on error
2046     service.test_barcode = function(bc) {
2047
2048         var ok = service.check_barcode(bc);
2049         if (ok) return $q.when();
2050
2051         egCore.audio.play('warning.circ.bad_barcode');
2052         return $uibModal.open({
2053             templateUrl: './circ/share/t_bad_barcode_dialog',
2054             backdrop: 'static',
2055             controller: 
2056                 ['$scope', '$uibModalInstance', 
2057                 function($scope, $uibModalInstance) {
2058                 $scope.barcode = bc;
2059                 $scope.ok = function() { $uibModalInstance.close() }
2060                 $scope.cancel = function() { $uibModalInstance.dismiss() }
2061             }]
2062         }).result;
2063     }
2064
2065     // check() and checkdigit() copied directly 
2066     // from chrome/content/util/barcode.js
2067
2068     service.check_barcode = function(bc) {
2069         if (bc != Number(bc)) return false;
2070         bc = bc.toString();
2071         // "16.00" == Number("16.00"), but the . is bad.
2072         // Throw out any barcode that isn't just digits
2073         if (bc.search(/\D/) != -1) return false;
2074         var last_digit = bc.substr(bc.length-1);
2075         var stripped_barcode = bc.substr(0,bc.length-1);
2076         return service.barcode_checkdigit(stripped_barcode).toString() == last_digit;
2077     }
2078
2079     service.barcode_checkdigit = function(bc) {
2080         var reverse_barcode = bc.toString().split('').reverse();
2081         var check_sum = 0; var multiplier = 2;
2082         for (var i = 0; i < reverse_barcode.length; i++) {
2083             var digit = reverse_barcode[i];
2084             var product = digit * multiplier; product = product.toString();
2085             var temp_sum = 0;
2086             for (var j = 0; j < product.length; j++) {
2087                 temp_sum += Number( product[j] );
2088             }
2089             check_sum += Number( temp_sum );
2090             multiplier = ( multiplier == 2 ? 1 : 2 );
2091         }
2092         check_sum = check_sum.toString();
2093         var next_multiple_of_10 = (check_sum.match(/(\d*)\d$/)[1] * 10) + 10;
2094         var check_digit = next_multiple_of_10 - Number(check_sum);
2095         if (check_digit == 10) check_digit = 0;
2096         return check_digit;
2097     }
2098
2099     service.handle_barcode_completion = function(barcode) {
2100         return egCore.net.request(
2101             'open-ils.actor',
2102             'open-ils.actor.get_barcodes',
2103             egCore.auth.token(), egCore.auth.user().ws_ou(), 
2104             'asset', barcode)
2105
2106         .then(function(resp) {
2107             // TODO: handle event during barcode lookup
2108             if (evt = egCore.evt.parse(resp)) {
2109                 console.error(evt.toString());
2110                 return $q.reject();
2111             }
2112
2113             // no matching barcodes: return the barcode as entered
2114             // by the user (so that, e.g., checkout can fall back to
2115             // precat/noncat handling)
2116             if (!resp || !resp[0]) {
2117                 return barcode;
2118             }
2119
2120             // exactly one matching barcode: return it
2121             if (resp.length == 1) {
2122                 return resp[0].barcode;
2123             }
2124
2125             // multiple matching barcodes: let the user pick one 
2126             console.debug('multiple matching barcodes');
2127             var matches = [];
2128             var promises = [];
2129             var final_barcode;
2130             angular.forEach(resp, function(cp) {
2131                 promises.push(
2132                     egCore.net.request(
2133                         'open-ils.circ',
2134                         'open-ils.circ.copy_details.retrieve',
2135                         egCore.auth.token(), cp.id
2136                     ).then(function(r) {
2137                         matches.push({
2138                             barcode: r.copy.barcode(),
2139                             title: r.mvr.title(),
2140                             org_name: egCore.org.get(r.copy.circ_lib()).name(),
2141                             org_shortname: egCore.org.get(r.copy.circ_lib()).shortname()
2142                         });
2143                     })
2144                 );
2145             });
2146             return $q.all(promises)
2147             .then(function() {
2148                 return $uibModal.open({
2149                     templateUrl: './circ/share/t_barcode_choice_dialog',
2150                     backdrop: 'static',
2151                     controller:
2152                         ['$scope', '$uibModalInstance',
2153                         function($scope, $uibModalInstance) {
2154                         $scope.matches = matches;
2155                         $scope.ok = function(barcode) {
2156                             $uibModalInstance.close();
2157                             final_barcode = barcode;
2158                         }
2159                         $scope.cancel = function() {$uibModalInstance.dismiss()}
2160                     }],
2161                 }).result.then(function() { return final_barcode });
2162             })
2163         });
2164     }
2165
2166     function generate_penalty_dialog_watch_callback($scope,egCore,allPenalties) {
2167         return function(newval) {
2168             if (newval) {
2169                 var selected_penalty = allPenalties.filter(function(p) {
2170                         return p.id() == newval; })[0];
2171                 var penalty_id = selected_penalty.id();
2172                 if (penalty_id == 20 || penalty_id == 21 || penalty_id == 25) {
2173                     $scope.args.custom_penalty = penalty_id;
2174                     $scope.args.penalty = penalty_id;
2175                 }
2176                 if (penalty_id > 100) {
2177                     $scope.args.custom_penalty = penalty_id;
2178                     $scope.args.penalty = null;
2179                 }
2180                 // there's a $watch on custom_depth
2181                 if (selected_penalty.org_depth() || selected_penalty.org_depth() == 0) {
2182                     $scope.args.custom_depth = selected_penalty.org_depth();
2183                 } else {
2184                     $scope.args.custom_depth = $scope.args.org.ou_type().depth();
2185                 }
2186             }
2187         };
2188     }
2189
2190     service.create_penalty = function(user_id) {
2191         return $uibModal.open({
2192             templateUrl: './circ/share/t_new_message_dialog',
2193             backdrop: 'static',
2194             controller: 
2195                    ['$scope','$uibModalInstance','allPenalties','goodOrgs',
2196             function($scope , $uibModalInstance , allPenalties , goodOrgs) {
2197                 $scope.focusNote = true;
2198                 $scope.penalties = allPenalties.filter(
2199                     function(p) { return p.id() > 100 || p.id() == 20 || p.id() == 21 || p.id() == 25; });
2200                 $scope.set_penalty = function(id) {
2201                     if (!($scope.args.pub && $scope.args.read_date) && !$scope.args.deleted) {
2202                         $scope.args.penalty = id;
2203                     }
2204                 }
2205                 $scope.require_initials = service.require_initials;
2206                 $scope.update_org = function(org) {
2207                     if (!($scope.args.pub && $scope.args.read_date) && !$scope.args.deleted) {
2208                         $scope.args.org = org;
2209                     }
2210                 }
2211                 $scope.cant_use_org = function(org_id) {
2212                     return ($scope.args.pub && $scope.args.read_date) || $scope.args.deleted || goodOrgs.indexOf(org_id) == -1;
2213                 }
2214                 $scope.args = {
2215                     pub : false,
2216                     penalty : 21, // default to Note
2217                     org : egCore.org.get(egCore.auth.user().ws_ou())
2218                 };
2219                 $scope.args.max_depth = $scope.args.org.ou_type().depth();
2220                 $scope.ok = function(count) { $uibModalInstance.close($scope.args) }
2221                 $scope.cancel = function($event) { 
2222                     $uibModalInstance.dismiss();
2223                     $event.preventDefault();
2224                 }
2225                 $scope.$watch('args.penalty', generate_penalty_dialog_watch_callback($scope,egCore,allPenalties));
2226                 $scope.$watch('args.custom_penalty', generate_penalty_dialog_watch_callback($scope,egCore,allPenalties));
2227                 $scope.$watch('args.custom_depth', function(org_depth) {
2228                     if (org_depth || org_depth == 0) {
2229                         egCore.net.request(
2230                             'open-ils.actor',
2231                             'open-ils.actor.org_unit.ancestor_at_depth.retrieve',
2232                             egCore.auth.token(), egCore.auth.user().ws_ou(), org_depth
2233                         ).then(function(ctx_org) {
2234                             if (ctx_org) {
2235                                 $scope.args.org = egCore.org.get(ctx_org);
2236                             }
2237                         });
2238                     }
2239                 });
2240             }],
2241             resolve : {
2242                 allPenalties : service.get_all_penalty_types,
2243                 goodOrgs : egCore.perm.hasPermAt('UPDATE_USER', true)
2244             }
2245         }).result.then(
2246             function(args) {
2247                 var pen = new egCore.idl.ausp();
2248                 var msg = {
2249                     pub : args.pub,
2250                     title : args.title,
2251                     message : args.note ? args.note : ''
2252                 };
2253                 pen.usr(user_id);
2254                 pen.org_unit(args.org.id());
2255                 if (args.initials) msg.message = (args.note ? args.note : '') + ' [' + args.initials + ']';
2256                 if (args.custom_penalty) {
2257                     pen.standing_penalty(args.custom_penalty);
2258                 } else {
2259                     pen.standing_penalty(args.penalty);
2260                 }
2261                 pen.staff(egCore.auth.user().id());
2262                 pen.set_date('now');
2263
2264                 return egCore.net.request(
2265                     'open-ils.actor',
2266                     'open-ils.actor.user.penalty.apply',
2267                     egCore.auth.token(), pen, msg
2268                 );
2269             }
2270         );
2271     }
2272
2273     // assumes, for now anyway,  penalty type is fleshed onto usr_penalty.
2274     service.edit_penalty = function(pen,aum) {
2275         return $uibModal.open({
2276             templateUrl: './circ/share/t_new_message_dialog',
2277             backdrop: 'static',
2278             controller: 
2279                    ['$scope','$uibModalInstance','allPenalties','goodOrgs',
2280             function($scope , $uibModalInstance , allPenalties , goodOrgs) {
2281                 // We may need to vivicate usr_penalty (pen) or usr_message (aum)
2282                 if (!pen) {
2283                     pen = new egCore.idl.ausp();
2284                     pen.usr(aum.usr());
2285                     pen.org_unit(aum.sending_lib()); // FIXME: preserve sending_lib or use ws_ou?
2286                     pen.staff(egCore.auth.user().id());
2287                     pen.set_date('now');
2288                     pen.usr_message(aum.id());
2289                     pen.isnew(true);
2290                     aum.ischanged(true);
2291                 }
2292                 if (!aum) {
2293                     aum = new egCore.idl.aum();
2294                     aum.create_date('now');
2295                     aum.sending_lib(pen.org_unit());
2296                     aum.pub(false);
2297                     aum.usr(pen.usr());
2298                     aum.isnew(true);
2299                     pen.ischanged(true);
2300                 }
2301
2302                 $scope.focusNote = true;
2303                 $scope.penalties = allPenalties.filter(
2304                     function(p) { return p.id() > 100 || p.id() == 20 || p.id() == 21 || p.id() == 25; });
2305                 $scope.set_penalty = function(id) {
2306                     if (!($scope.args.pub && $scope.args.read_date) && !$scope.args.deleted) {
2307                         $scope.args.penalty = id;
2308                     }
2309                 }
2310                 $scope.require_initials = service.require_initials;
2311                 $scope.update_org = function(org) {
2312                     if (!($scope.args.pub && $scope.args.read_date) && !$scope.args.deleted) {
2313                         $scope.args.org = org;
2314                     }
2315                 }
2316                 $scope.cant_use_org = function(org_id) {
2317                     return ($scope.args.pub && $scope.args.read_date) || $scope.args.deleted || goodOrgs.indexOf(org_id) == -1;
2318                 }
2319                 var penalty_id = pen.standing_penalty();
2320                 $scope.args = {
2321                     penalty : pen.isnew()
2322                         ? 21 // default to Note
2323                         : penalty_id,
2324                     pub : typeof aum.pub() == 'boolean'
2325                         ? aum.pub()
2326                         : aum.pub() == 't',
2327                     title : aum.title(),
2328                     note : aum.message() ? aum.message() : '',
2329                     org : egCore.org.get(pen.org_unit()),
2330                     deleted : typeof aum.deleted() == 'boolean'
2331                         ? aum.deleted()
2332                         : aum.deleted() == 't',
2333                     read_date : aum.read_date(),
2334                     edit_date : aum.edit_date(),
2335                     stop_date : aum.stop_date(),
2336                     editor : aum.editor()
2337                 }
2338                 $scope.args.max_depth = $scope.args.org.ou_type().depth();
2339                 $scope.original_org = $scope.args.org;
2340                 $scope.workstation_depth = egCore.org.get(egCore.auth.user().ws_ou()).ou_type().depth();
2341                 if (penalty_id == 20 || penalty_id == 21 || penalty_id == 25) {
2342                     $scope.args.custom_penalty = penalty_id;
2343                 }
2344                 if (penalty_id > 100) {
2345                     $scope.args.custom_penalty = penalty_id;
2346                     $scope.args.penalty = null;
2347                 }
2348                 $scope.ok = function(count) { $uibModalInstance.close($scope.args) }
2349                 $scope.cancel = function($event) { 
2350                     $uibModalInstance.dismiss();
2351                     $event.preventDefault();
2352                 }
2353                 $scope.$watch('args.penalty', generate_penalty_dialog_watch_callback($scope,egCore,allPenalties));
2354                 $scope.$watch('args.custom_penalty', generate_penalty_dialog_watch_callback($scope,egCore,allPenalties));
2355                 $scope.$watch('args.custom_depth', function(org_depth) {
2356                     if (org_depth || org_depth == 0) {
2357                         if (org_depth > $scope.workstation_depth) {
2358                             $scope.args.org = $scope.original_org;
2359                         } else {
2360                             egCore.net.request(
2361                                 'open-ils.actor',
2362                                 'open-ils.actor.org_unit.ancestor_at_depth.retrieve',
2363                                 egCore.auth.token(), egCore.auth.user().ws_ou(), org_depth
2364                             ).then(function(ctx_org) {
2365                                 if (ctx_org) {
2366                                     $scope.args.org = egCore.org.get(ctx_org);
2367                                 }
2368                             });
2369                         }
2370                     }
2371                 });
2372             }],
2373             resolve : {
2374                 allPenalties : service.get_all_penalty_types,
2375                 goodOrgs : egCore.perm.hasPermAt('UPDATE_USER', true)
2376             }
2377         }).result.then(
2378             function(args) {
2379                 aum.pub(args.pub);
2380                 aum.title(args.title);
2381                 aum.message(args.note);
2382                 aum.sending_lib(egCore.org.get(egCore.auth.user().ws_ou()).id());
2383                 pen.org_unit(egCore.org.get(args.org).id());
2384                 if (args.initials) aum.message((args.note ? args.note : '') + ' [' + args.initials + ']');
2385                 if (args.custom_penalty) {
2386                     pen.standing_penalty(args.custom_penalty);
2387                 } else {
2388                     pen.standing_penalty(args.penalty);
2389                 }
2390                 return egCore.net.request(
2391                     'open-ils.actor',
2392                     'open-ils.actor.user.penalty.modify',
2393                     egCore.auth.token(), pen, aum
2394                 );
2395             }
2396         );
2397     }
2398
2399     return service;
2400
2401 }]);
2402
2403