2 * Checkin, checkout, and renew
5 angular.module('egCoreMod')
9 ['$uibModal','$q','egCore','egAlertDialog','egConfirmDialog','egAddCopyAlertDialog','egCopyAlertManagerDialog','egCopyAlertEditorDialog',
11 function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, egAddCopyAlertDialog , egCopyAlertManagerDialog, egCopyAlertEditorDialog ,
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,
21 hold_shelf_slip : false,
22 hold_transit_slip : false,
25 in_flight_checkins: {}
28 egCore.startup.go().finally(function() {
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']);
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;
50 service.reset = function() {
51 service.auto_override_circ_events = {};
52 service.auto_skip_circ_events = {};
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',
61 'PATRON_CARD_INACTIVE',
62 'PATRON_ACCOUNT_EXPIRED',
63 'PERM_FAILURE' // should be handled elsewhere
66 // Default to checked for "Automatically override for subsequent items?"
67 service.default_auto_override = [
68 'PATRON_EXCEEDS_OVERDUE_COUNT',
70 'PATRON_EXCEEDS_LOST_COUNT',
71 'PATRON_EXCEEDS_CHECKOUT_COUNT',
72 'PATRON_EXCEEDS_FINES',
73 'PATRON_EXCEEDS_LONGOVERDUE_COUNT'
76 // these checkin events do not produce alerts when
77 // options.suppress_alerts is in effect.
78 service.checkin_suppress_overrides = [
82 'PATRON_ACCOUNT_EXPIRED',
84 'CIRC_CLAIMS_RETURNED',
87 'COPY_STATUS_LOST_AND_PAID',
88 'COPY_STATUS_LONG_OVERDUE',
89 'COPY_STATUS_MISSING',
90 'PATRON_EXCEEDS_FINES'
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'
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.
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;
110 console.debug('egCirc.checkout() : '
111 + js2JSON(params) + ' : ' + js2JSON(options));
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;
119 var promise = options.check_barcode ?
120 service.test_barcode(params.copy_barcode) : $q.when();
122 // avoid re-check on override, etc.
123 delete options.check_barcode;
125 return promise.then(function() {
127 var method = 'open-ils.circ.checkout.full';
128 if (options.override) method += '.override';
130 return egCore.net.request(
131 'open-ils.circ', method, egCore.auth.token(), params
133 ).then(function(evt) {
135 if (!angular.isArray(evt)) evt = [evt];
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);
144 var action = params.noncat ? 'noncat_checkout' : 'checkout';
146 return service.flesh_response_data(action, evt, params, options)
148 return service.handle_checkout_resp(evt, params, options);
150 .then(function(final_resp) {
151 return service.munge_resp_data(final_resp,action,method)
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;
166 console.debug('egCirc.renew() : '
167 + js2JSON(params) + ' : ' + js2JSON(options));
169 // handle barcode completion
170 return service.handle_barcode_completion(params.copy_barcode)
171 .then(function(barcode) {
172 params.copy_barcode = barcode;
174 var promise = options.check_barcode ?
175 service.test_barcode(params.copy_barcode) : $q.when();
177 // avoid re-check on override, etc.
178 delete options.check_barcode;
180 return promise.then(function() {
182 var method = 'open-ils.circ.renew';
183 if (options.override) method += '.override';
185 return egCore.net.request(
186 'open-ils.circ', method, egCore.auth.token(), params
188 ).then(function(evt) {
190 if (!angular.isArray(evt)) evt = [evt];
192 return service.flesh_response_data(
193 'renew', evt, params, options)
195 return service.handle_renew_resp(evt, params, options);
197 .then(function(final_resp) {
198 final_resp.auto_renew = options.auto_renew;
199 return service.munge_resp_data(final_resp,'renew',method)
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;
214 console.debug('egCirc.checkin() : '
215 + js2JSON(params) + ' : ' + js2JSON(options));
217 // handle barcode completion
218 return service.handle_barcode_completion(params.copy_barcode)
219 .then(function(barcode) {
220 params.copy_barcode = barcode;
222 var promise = options.check_barcode ?
223 service.test_barcode(params.copy_barcode) : $q.when();
225 // avoid re-check on override, etc.
226 delete options.check_barcode;
228 return promise.then(function() {
230 var method = 'open-ils.circ.checkin';
231 if (options.override) method += '.override';
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');
240 service.in_flight_checkins[barcode] = true;
242 return egCore.net.request(
243 'open-ils.circ', method, egCore.auth.token(), params
245 ).then(function(evt) {
246 delete service.in_flight_checkins[barcode];
248 if (!angular.isArray(evt)) evt = [evt];
249 return service.flesh_response_data(
250 'checkin', evt, params, options)
252 return service.handle_checkin_resp(evt, params, options);
254 .then(function(final_resp) {
255 return service.munge_resp_data(final_resp,'checkin',method)
257 }, function() {delete service.in_flight_checkins[barcode]});
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 = {};
266 if (!final_resp.evt[0]) {
267 egCore.audio.play('error.unknown.no_event');
271 var payload = final_resp.evt[0].payload;
273 egCore.audio.play('error.unknown.no_payload');
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)});
281 if (payload.volume && typeof payload.volume.suffix() != 'object') {
282 egCore.pcrud.retrieve('acns',payload.volume.suffix()).then(function(s) {payload.volume.suffix(s)});
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;
301 if (payload.circ) data.duration = payload.circ.duration();
302 if (payload.circ) data.circ_lib = payload.circ.circ_lib();
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();
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();
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();
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) {
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'
333 'action' : worklog_action,
334 'method' : worklog_method,
335 'response' : final_resp
342 service.handle_overridable_checkout_event = function(evt, params, options) {
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); });
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);
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.
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');
370 return service.override_dialog(evt, params, options, 'checkout');
374 service.handle_overridable_renew_event = function(evt, params, options) {
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); });
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);
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.
397 switch(evt[0].textcode) {
398 case 'COPY_ALERT_MESSAGE':
399 return service.copy_alert_dialog(evt[0], params, options, 'renew');
401 return service.override_dialog(evt, params, options, 'renew');
406 service.handle_overridable_checkin_event = function(evt, params, options) {
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); });
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);
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.
428 switch(evt[0].textcode) {
429 case 'COPY_ALERT_MESSAGE':
430 return service.copy_alert_dialog(evt[0], params, options, 'checkin');
432 return service.override_dialog(evt, params, options, 'checkin');
437 service.handle_renew_resp = function(evt, params, options) {
439 var final_resp = {evt : evt, params : params, options : options};
441 // track the barcode regardless of whether it refers to a copy
442 angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
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);
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);
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');
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,
482 service.handle_checkout_resp = function(evt, params, options) {
484 var final_resp = {evt : evt, params : params, options : options};
486 // track the barcode regardless of whether it refers to a copy
487 angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
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);
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);
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);
506 case 'COPY_IN_TRANSIT':
507 egCore.audio.play('warning.checkout.in_transit');
508 return service.copy_in_transit_dialog(evt, params, options);
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);
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');
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,
541 // returns a promise resolved with the list of circ mods
542 service.get_circ_mods = function() {
544 return $q.when(egCore.env.ccm.list);
546 return egCore.pcrud.retrieveAll('ccm', null, {atomic : true})
547 .then(function(list) {
548 egCore.env.absorbList(list, 'ccm');
553 // returns a promise resolved with the list of noncat types
554 service.get_noncat_types = function() {
556 return $q.when(egCore.env.cnct.list);
558 return egCore.pcrud.search('cnct',
560 egCore.org.fullPath(egCore.auth.user().ws_ou(), true)},
561 null, {atomic : true}
562 ).then(function(list) {
563 egCore.env.absorbList(list, 'cnct');
568 service.get_all_penalty_types = function() {
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;
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) {
583 if (!evt[0] || !(payload = evt[0].payload)) return $q.when();
585 promises.push(service.flesh_copy_location(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));
591 service.flesh_copy_status(payload.copy)
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) {
601 trans.source(egCore.org.get(trans.source()));
602 trans.dest(egCore.org.get(trans.dest()));
603 payload.transit = trans;
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()));
617 // TODO: renewal responses should include the patron
618 if (!payload.patron) {
620 if (payload.circ) user_id = payload.circ.usr();
621 if (payload.noncat_circ) user_id = payload.noncat_circ.patron();
624 egCore.pcrud.retrieve('au', user_id)
625 .then(function(user) {payload.patron = user})
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);});
634 angular.forEach(evt, function(e){ e.author = payload.record ? payload.record.author() :
635 (payload.copy ? payload.copy.dummy_author() : null);});
637 angular.forEach(evt, function(e){ e.isbn = payload.record ? payload.record.isbn() :
638 (payload.copy ? payload.copy.dummy_isbn() : null);});
640 return $q.all(promises);
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() )));
648 service.flesh_copy_circ_library = function(copy) {
649 if (!copy) return $q.when();
651 return $q.when(copy.circ_lib(egCore.org.get( copy.circ_lib() )));
654 // fetches the full list of circ modifiers
655 service.flesh_copy_circ_modifier = function(copy) {
656 if (!copy) return $q.when();
658 return $q.when(copy.circ_modifier(egCore.env.ccm.map[copy.circ_modifier()]));
659 return egCore.pcrud.retrieveAll('ccm', {}, {atomic : true}).then(
661 egCore.env.absorbList(list, 'ccm');
662 copy.circ_modifier(egCore.env.ccm.map[copy.circ_modifier()]);
667 // fetches the full list of copy statuses
668 service.flesh_copy_status = function(copy) {
669 if (!copy) return $q.when();
671 return $q.when(copy.status(egCore.env.ccs.map[copy.status()]));
672 return egCore.pcrud.retrieveAll('ccs', {}, {atomic : true}).then(
674 egCore.env.absorbList(list, 'ccs');
675 copy.status(egCore.env.ccs.map[copy.status()]);
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);
691 return egCore.pcrud.retrieve('acpl', copy.location())
692 .then(function(loc) {
693 egCore.env.absorbList([loc], 'acpl'); // append to cache
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]();
705 if (!addr_id) return $q.when(null);
707 if (egCore.env.aoa && egCore.env.aoa.map[addr_id])
708 return $q.when(egCore.env.aoa.map[addr_id]);
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];
716 //retrieve addresses from multiple org units
717 service.cache_org_addr = function (org_ids, addr_type) {
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);
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');
730 service.exit_alert = function(msg, scope) {
731 return egAlertDialog.open(msg, scope).result.then(
732 function() {return $q.reject()});
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];
740 egCore.audio.play('warning.circ.event_override');
741 var copy_alert = evt.filter(function(e) {
742 return e.textcode == 'COPY_ALERT_MESSAGE';
744 evt = evt.filter(function(e) {
745 return e.textcode !== 'COPY_ALERT_MESSAGE';
748 return $uibModal.open({
749 templateUrl: './circ/share/t_event_override_dialog',
752 ['$scope', '$uibModalInstance',
753 function($scope, $uibModalInstance) {
755 $scope.action = action;
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'
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];
769 $scope.patronName = $scope.holdEvent.payload.patron_name;
770 $scope.holdID = $scope.holdEvent.payload.hold_id;
771 $scope.patronID = $scope.holdEvent.payload.patron_id;
774 $scope.copy_barcode = params.copy_barcode; // may be null
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
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
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
795 : service.auto_skip_circ_events[e.textcode]
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
803 service.nonoverridable_events.indexOf(e.textcode) > -1
805 : service.default_auto_override.indexOf(e.textcode) > -1
807 : service.auto_override_circ_events[e.textcode]
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;
823 // shrink the skip list
824 service.auto_skip_circ_events[e.textcode] = false;
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;
833 // shrink the auto-override list
834 service.auto_override_circ_events[e.textcode] = false;
839 window.oils_auto_skip_circ_events = service.auto_skip_circ_events;
840 window.oils_auto_override_circ_events = service.auto_override_circ_events;
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) {
849 'open-ils.circ.hold.cancel',
850 egCore.auth.token(), $scope.holdID,
852 'Item checked out by other patron' // FIXME I18n
855 $uibModalInstance.close();
858 $scope.skip = function($event) {
859 update_auto_override_and_skip_lists();
860 $uibModalInstance.dismiss();
861 $event.preventDefault();
864 $scope.cancel = function ($event) {
865 window.oils_cancel_batch = true;
866 $uibModalInstance.dismiss();
867 $event.preventDefault();
872 options.override = true;
874 if (copy_alert.length > 0) {
875 return service.copy_alert_dialog(copy_alert, params, options, action);
878 if (action == 'checkin') {
879 return service.checkin(params, options);
882 return service[action](params, options);
887 service.copy_not_avail_dialog = function(evt, params, options) {
888 if (!angular.isArray(evt)) evt = [evt];
890 var copy_alert = evt.filter(function(e) {
891 return e.textcode == 'COPY_ALERT_MESSAGE';
893 evt = evt.filter(function(e) {
894 return e.textcode !== 'COPY_ALERT_MESSAGE';
898 return $uibModal.open({
899 templateUrl: './circ/share/t_copy_not_avail_dialog',
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()}
909 copyStatus : function() {
910 return egCore.pcrud.retrieve(
911 'ccs', evt.payload.status());
916 options.override = true;
918 if (copy_alert.length > 0) {
919 return service.copy_alert_dialog(copy_alert, params, options, 'checkout');
922 return service.checkout(params, options);
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.
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
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() {
941 params.noncat = true;
942 var type = egCore.env.cnct.map[params.noncat_type];
944 return $uibModal.open({
945 templateUrl: './circ/share/t_noncat_dialog',
948 ['$scope', '$uibModalInstance',
949 function($scope, $uibModalInstance) {
950 $scope.focusMe = true;
953 $scope.noncatMax = noncatMax;
954 $scope.ok = function(count) { $uibModalInstance.close(count) }
955 $scope.cancel = function ($event) {
956 $uibModalInstance.dismiss()
957 $event.preventDefault();
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);
974 // Opens a dialog allowing the user to fill in pre-cat copy info.
975 service.precat_dialog = function(params, options) {
977 return $uibModal.open({
978 templateUrl: './circ/share/t_precat_dialog',
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
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() }
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();
1001 circMods : function() { return service.get_circ_mods(); },
1002 has_precat_perm : function(){ return egCore.perm.hasPermHere('CREATE_PRECAT'); }
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);
1015 // find the open transit for the given copy barcode; flesh the org
1017 service.find_copy_transit = function(evt, params, options) {
1018 if (angular.isArray(evt)) evt = evt[0];
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.
1024 return egCore.pcrud.search('atc',
1025 { dest_recv_time : null, cancel_time : null},
1027 flesh_fields : {atc : ['target_copy']},
1031 barcode : params.copy_barcode,
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()));
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',
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() }
1059 // fetch the conflicting open transit w/ fleshed copy
1060 transit : function() {
1061 return service.find_copy_transit(evt, params, options);
1066 // user chose to abort the transit then checkout
1067 return service.abort_transit(transit.id())
1069 return service.checkout(params, options);
1075 service.abort_transit = function(transit_id) {
1076 return egCore.net.request(
1078 'open-ils.circ.transit.abort',
1079 egCore.auth.token(), {transitid : transit_id}
1080 ).then(function(resp) {
1081 if (evt = egCore.evt.parse(resp)) {
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}
1096 service.circ_exists_dialog = function(evt, params, options) {
1097 if (angular.isArray(evt)) evt = evt[0];
1099 if (!evt.payload.old_circ) {
1100 return egCore.net.request(
1102 'open-ils.search.asset.copy.fleshed2.find_by_barcode',
1104 ).then(function(resp){
1106 if (egCore.evt.parse(resp)) {
1107 console.error(egCore.evt.parse(resp));
1109 return egCore.net.request(
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 );
1120 return service.circ_exists_dialog_impl( evt, params, options );
1124 service.circ_exists_dialog_impl = function (evt, params, options) {
1126 var openCirc = evt.payload.old_circ;
1127 var sameUser = openCirc.usr() == params.patron_id;
1129 return $uibModal.open({
1130 templateUrl: './circ/share/t_circ_exists_dialog',
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();
1147 params.void_overdues = args.forgive_fines;
1148 options.sameCopyCheckout = true;
1149 return service.renew(params, options);
1152 return service.checkin({
1153 barcode : params.copy_barcode,
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);
1160 alert(egCore.evt.parse(checkin_resp.evt[0]));
1168 service.batch_backdate = function(circ_ids, backdate) {
1169 return egCore.net.request(
1171 'open-ils.circ.post_checkin_backdate.batch',
1172 egCore.auth.token(), circ_ids, backdate);
1175 service.backdate_dialog = function(circ_ids) {
1176 return $uibModal.open({
1177 templateUrl: './circ/share/t_backdate_dialog',
1180 ['$scope','$uibModalInstance',
1181 function($scope , $uibModalInstance) {
1183 var today = new Date();
1185 num_circs : circ_ids.length,
1190 $scope.$watch('dialog.backdate', function(newval) {
1191 if (newval && newval > today)
1192 $scope.dialog.backdate = today;
1196 $scope.cancel = function() {
1197 $uibModalInstance.dismiss();
1200 $scope.ok = function() {
1202 var bd = $scope.dialog.backdate.toISOString().replace(/T.*/,'');
1203 service.batch_backdate(circ_ids, bd)
1205 function() { // on complete
1206 $uibModalInstance.close({backdate : bd});
1209 function(resp) { // on response
1210 console.debug('backdate returned ' + resp);
1212 $scope.num_processed++;
1214 console.error(egCore.evt.parse(resp));
1223 service.mark_claims_returned = function(barcode, date, override) {
1225 var method = 'open-ils.circ.circulation.set_claims_returned';
1226 if (override) method += '.override';
1228 console.debug('claims returned ' + method);
1230 return egCore.net.request(
1231 'open-ils.circ', method, egCore.auth.token(),
1232 {barcode : barcode, backdate : date})
1234 .then(function(resp) {
1236 if (resp == 1) { // success
1237 console.debug('claims returned succeeded for ' + barcode);
1240 } else if (evt = egCore.evt.parse(resp)) {
1241 console.debug('claims returned failed: ' + evt.toString());
1243 if (evt.textcode == 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT') {
1244 // TODO check perms before offering override option?
1246 if (override) return;// just to be safe
1248 return egConfirmDialog.open(
1249 egCore.strings.TOO_MANY_CLAIMS_RETURNED, '', {}
1250 ).result.then(function() {
1251 return service.mark_claims_returned(barcode, date, true);
1255 if (evt.textcode == 'PERM_FAILURE') {
1256 console.error('claims returned permission denied')
1257 // TODO: auth override dialog?
1263 service.mark_claims_returned_dialog = function(copy_barcodes) {
1264 if (!copy_barcodes.length) return;
1266 return $uibModal.open({
1267 templateUrl: './circ/share/t_mark_claims_returned_dialog',
1270 ['$scope','$uibModalInstance',
1271 function($scope , $uibModalInstance) {
1273 var today = new Date();
1275 barcodes : copy_barcodes,
1279 $scope.$watch('args.date', function(newval) {
1280 if (newval && newval > today)
1281 $scope.args.backdate = today;
1284 $scope.cancel = function() {$uibModalInstance.dismiss()}
1285 $scope.ok = function() {
1287 var date = $scope.args.date.toISOString().replace(/T.*/,'');
1289 var deferred = $q.defer();
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();
1297 $uibModalInstance.close();
1301 // finally -> continue even when one fails
1302 service.mark_claims_returned(bc, date)
1303 .finally(function(barcode) {
1304 if (barcode) deferred.notify(barcode);
1308 mark_one(); // kick it off
1309 return deferred.promise;
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;
1321 var deferred = $q.defer();
1322 egConfirmDialog.open(
1323 egCore.strings.MARK_NEVER_CHECKED_OUT, '', {barcodes : barcodes}
1325 ).result.then(function() {
1326 function mark_one() {
1327 var bc = barcodes.pop();
1329 if (!bc) { // all done
1335 {claims_never_checked_out : true, copy_barcode : bc})
1336 .finally(function() {
1337 deferred.notify(bc);
1344 return deferred.promise;
1347 service.mark_damaged = function(params) {
1348 if (!params) return $q.when();
1349 return $uibModal.open({
1351 templateUrl: './circ/share/t_mark_damaged',
1353 ['$scope', '$uibModalInstance', 'egCore', 'egBilling', 'egItem',
1354 function($scope, $uibModalInstance, egCore, egBilling, egItem) {
1355 var doRefresh = params.refresh;
1357 $scope.showBill = params.charge != null && params.circ;
1358 $scope.billArgs = {charge: params.charge};
1359 $scope.mode = 'charge';
1360 $scope.barcode = params.barcode;
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();
1368 egBilling.fetchBillingTypes().then(function(res) {
1369 $scope.billingTypes = res;
1372 $scope.btnChargeFees = function() {
1373 $scope.mode = 'charge';
1374 $scope.billArgs.charge = params.charge;
1376 $scope.btnWaiveFees = function() {
1377 $scope.mode = 'waive';
1378 $scope.billArgs.charge = 0;
1381 $scope.cancel = function ($event) {
1382 $uibModalInstance.dismiss();
1384 $scope.ok = function() {
1385 handle_mark_item_damaged();
1388 var handle_mark_item_damaged = function() {
1390 if ($scope.showBill)
1391 applyFines = $scope.billArgs.charge ? 'apply' : 'noapply';
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)) {
1405 console.debug("mark damaged more information required. Pushing back.");
1406 service.mark_damaged({
1408 barcode: params.barcode,
1409 charge: evt.payload.charge,
1410 circ: evt.payload.circ,
1411 refresh: params.refresh
1413 console.error('mark damaged failed: ' + evt);
1415 }).then(function() {
1416 if (doRefresh) egItem.add_barcode_to_list(params.barcode);
1418 $uibModalInstance.close();
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
1431 'Mark item ' + status.name() + ' for ' +
1432 copy.barcode + ' failed: ' + event
1434 return service.exit_alert(
1435 egCore.strings.MARK_MISSING_FAILURE_CHECKED_OUT,
1436 {barcode : copy.barcode});
1438 dlogTitle = egCore.strings.MARK_ITEM_CHECKED_OUT;
1439 dlogMessage = egCore.strings.MARK_ITEM_CHECKIN_CONTINUE;
1440 args.handle_checkin = 1;
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;
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;
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;
1457 case 'PERM_FAILURE':
1458 console.error('Mark item ' + status.name() + ' for ' + copy.barcode + ' failed: ' +
1460 return service.exit_alert(egCore.strings.PERMISSION_DENIED,
1461 {permission : event.ilsperm});
1464 console.error('Mark item ' + status.name() + ' for ' + copy.barcode + ' failed: ' +
1466 return service.exit_alert(egCore.strings.MARK_ITEM_FAILURE,
1467 {status : status.name(), barcode : copy.barcode,
1468 textcode : event.textcode});
1471 return egConfirmDialog.open(
1472 dlogTitle, dlogMessage,
1474 barcode : copy.barcode,
1475 status : status.name(),
1476 ok : function () {},
1477 cancel : function () {}
1479 ).result.then(function() {
1480 return service.mark_item(copy, status, args);
1484 service.mark_item = function(copy, markstatus, args) {
1485 if (!copy) return $q.when();
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.
1491 switch (markstatus.id()) {
1493 // Not implemented in the staff client, yet.
1494 // req = "open-ils.circ.mark_item_bindery";
1497 req = "open-ils.circ.mark_item_missing";
1500 // Not implemented in the staff client, yet.
1501 // req = "open-ils.circ.mark_item_on_order";
1504 // Not implemented in the staff client, yet.
1505 // req = "open-ils.circ.mark_item_ill";
1508 // Not implemented in the staff client, yet.
1509 // req = "open-ils.circ.mark_item_cataloging";
1512 // Not implemented in the staff client, yet.
1513 // req = "open-ils.circ.mark_item_reserves";
1516 req = "open-ils.circ.mark_item_discard";
1519 // Damaged is for handling of events. It's main handler is elsewhere.
1520 req = "open-ils.circ.mark_item_damaged";
1524 return egCore.net.request(
1527 egCore.auth.token(),
1530 ).then(function(resp) {
1531 if (evt = egCore.evt.parse(resp)) {
1532 return service.handle_mark_item_event(copy, markstatus, args, evt);
1537 service.mark_discard = function(copies) {
1538 return egConfirmDialog.open(
1539 egCore.strings.MARK_DISCARD_CONFIRM, '',
1541 num_items : copies.length,
1543 cancel : function() {}
1545 ).result.then(function() {
1546 return egCore.pcrud.retrieve('ccs', 13)
1547 .then(function(resp) {
1549 angular.forEach(copies, function(copy) {
1550 promises.push(service.mark_item(copy, resp, {}))
1552 return $q.all(promises);
1557 service.mark_missing = function(copies) {
1558 return egConfirmDialog.open(
1559 egCore.strings.MARK_MISSING_CONFIRM, '',
1561 num_items : copies.length,
1563 cancel : function() {}
1565 ).result.then(function() {
1566 return egCore.pcrud.retrieve('ccs', 4)
1567 .then(function(resp) {
1569 var promise = $q.when();
1570 angular.forEach(copies, function(copy) {
1571 promise = promise.then(function() {
1572 return service.mark_item(
1575 modified.push(copy.barcode);
1576 }).catch(function(){});
1579 promise = promise.then(function() {
1580 if (!modified.length) return $q.reject();
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();
1597 angular.forEach(copy_barcodes, function(barcode) {
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());
1608 // inform the caller as each item is processed
1609 deferred.notify(barcode);
1614 $q.all(promises).then(function() {deferred.resolve()});
1615 return deferred.promise;
1618 service.abort_transits = function(transit_ids) {
1619 return egConfirmDialog.open(
1620 egCore.strings.ABORT_TRANSIT_CONFIRM, '',
1621 { num_transits : transit_ids.length,
1623 cancel : function() {}
1626 ).result.then(function() {
1628 angular.forEach(transit_ids, function(transit_id) {
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);
1642 return $q.all(promises);
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() { });
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() { });
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];
1667 var copy = evt && evt.payload ? evt.payload.copy : null;
1669 if (copy && !options.suppress_popups
1670 && copy.location().checkin_alert() == 't') {
1672 return egAlertDialog.open(
1673 egCore.strings.LOCATION_ALERT_MSG, {copy : copy}).result;
1679 service.handle_checkin_resp = function(evt, params, options) {
1680 if (!angular.isArray(evt)) evt = [evt];
1682 var final_resp = {evt : evt, params : params, options : options};
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;
1691 // track the barcode regardless of whether it's valid
1692 angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
1694 angular.forEach(evt, function(e){ console.debug('checkin event ' + e.textcode); });
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);
1699 switch (evt[0].textcode) {
1704 switch(Number(copy.status().id())) {
1706 case 0: /* AVAILABLE */
1707 case 4: /* MISSING */
1708 case 7: /* RESHELVING */
1710 egCore.audio.play('success.checkin');
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});
1716 case 8: /* ON HOLDS SHELF */
1717 egCore.audio.play('info.checkin.holds_shelf');
1721 if (hold.pickup_lib() == egCore.auth.user().ws_ou()) {
1722 // inform user if the item is on the local holds shelf
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 });
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);
1739 console.error('checkin: item on holds shelf, '
1740 + 'but hold info not returned from checkin');
1741 return $q.when(final_resp);
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});
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);
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);
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();
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});
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});
1792 case 'HOLD_CAPTURE_DELAYED':
1793 return service.hold_capture_delay_dialog(
1794 evt[0], params, options, 'checkin');
1797 egCore.audio.play('error.checkin.unknown');
1798 console.warn('unhandled checkin response : ' + evt[0].textcode);
1799 return $q.when(final_resp);
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];
1809 var addr_deferred = $q.defer();
1810 // associates org units with the address they're needed for
1812 promises.push(addr_deferred.promise);
1814 if (evt.org && !tmpl.match(/hold_shelf/)) {
1815 addr_orgs['address'] = evt.org;
1818 if(evt.payload.transit){
1819 addr_orgs['source_address'] = evt.payload.transit.source().id();
1822 if(Object.keys(addr_orgs).length){
1824 service.cache_org_addr(Object.values(addr_orgs),'holds_address')
1826 // promise to assign all of the addresses we need
1827 var addr_promises = [];
1828 Object.keys(addr_orgs).forEach(function(key){
1830 service.get_org_addr(addr_orgs[key], 'holds_address')
1831 .then(function(addr) {
1832 // assign address to field in data
1837 $q.all(addr_promises).then(addr_deferred.resolve());
1841 // no addresses are needed so continue
1842 addr_deferred.resolve();
1845 if (evt.payload.hold) {
1847 egCore.pcrud.retrieve('au',
1848 evt.payload.hold.usr(), {
1850 flesh_fields : {'au' : ['card', 'profile']}
1852 ).then(function(patron) {data.patron = patron})
1857 if (!tmpl.match(/hold_shelf/)) {
1858 var courier_deferred = $q.defer();
1859 promises.push(courier_deferred.promise);
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())
1866 data.dest_courier_code = s['lib.courier_code'];
1867 courier_deferred.resolve();
1873 return $q.all(promises).then(function() { return data });
1876 service.route_dialog = function(tmpl, evt, params, options) {
1877 if (angular.isArray(evt)) evt = evt[0];
1879 return service.collect_route_data(tmpl, evt, params, options)
1880 .then(function(data) {
1882 var template = data.transit ?
1883 (data.patron ? 'hold_transit_slip' : 'transit_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
1892 // All actions flow from the print data
1894 var print_context = {
1895 copy : egCore.idl.toHash(evt.payload.copy),
1897 author : evt.author,
1898 call_number : egCore.idl.toHash(evt.payload.volume)
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 = "";
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;
1911 print_context.dest_address = egCore.idl.toHash(data.address);
1913 if (data.source_address) {
1914 print_context.source_address = egCore.idl.toHash(data.source_address);
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);
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);
1932 print_context.patron = egCore.idl.toHash(data.patron);
1935 var sound = 'info.checkin.transit';
1936 if (evt.payload.hold) sound += '.hold';
1937 egCore.audio.play(sound);
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 });
1947 // when auto-print is on, skip the dialog and go straight
1949 if (options.auto_print_holds_transits || options.suppress_popups)
1950 return print_transit(template);
1952 return $uibModal.open({
1956 '$scope','$uibModalInstance',
1957 function($scope , $uibModalInstance) {
1959 $scope.today = new Date();
1961 // copy the print scope into the dialog scope
1962 angular.forEach(print_context, function(val, key) {
1966 $scope.ok = function() {$uibModalInstance.close()}
1968 $scope.print = function() {
1969 $uibModalInstance.close();
1970 print_transit(template);
1974 }).result.then(function() { return data });
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,
1988 cancel : function() {}
1990 ).result.then(function() {
1991 options.override = true;
1992 return service[action](params, options);
1994 } else { // we got a list of copy alert objects ...
1995 return egCopyAlertManagerDialog.open({
1996 alerts : evt.payload,
1998 ok : function(the_next_status) {
1999 if (the_next_status !== null) {
2000 params.next_copy_status = [ the_next_status ];
2001 params.capture = 'nocapture';
2004 cancel : function() {}
2005 }).result.then(function() {
2006 options.override = true;
2007 return service[action](params, options);
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',
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();
2026 $scope.nocapture = function() {
2027 params.capture = 'nocapture';
2028 $uibModalInstance.close();
2030 $scope.cancel = function() { $uibModalInstance.dismiss(); };
2033 params : function() {
2039 return service[action](params, options);
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) {
2048 var ok = service.check_barcode(bc);
2049 if (ok) return $q.when();
2051 egCore.audio.play('warning.circ.bad_barcode');
2052 return $uibModal.open({
2053 templateUrl: './circ/share/t_bad_barcode_dialog',
2056 ['$scope', '$uibModalInstance',
2057 function($scope, $uibModalInstance) {
2058 $scope.barcode = bc;
2059 $scope.ok = function() { $uibModalInstance.close() }
2060 $scope.cancel = function() { $uibModalInstance.dismiss() }
2065 // check() and checkdigit() copied directly
2066 // from chrome/content/util/barcode.js
2068 service.check_barcode = function(bc) {
2069 if (bc != Number(bc)) return false;
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;
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();
2086 for (var j = 0; j < product.length; j++) {
2087 temp_sum += Number( product[j] );
2089 check_sum += Number( temp_sum );
2090 multiplier = ( multiplier == 2 ? 1 : 2 );
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;
2099 service.handle_barcode_completion = function(barcode) {
2100 return egCore.net.request(
2102 'open-ils.actor.get_barcodes',
2103 egCore.auth.token(), egCore.auth.user().ws_ou(),
2106 .then(function(resp) {
2107 // TODO: handle event during barcode lookup
2108 if (evt = egCore.evt.parse(resp)) {
2109 console.error(evt.toString());
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]) {
2120 // exactly one matching barcode: return it
2121 if (resp.length == 1) {
2122 return resp[0].barcode;
2125 // multiple matching barcodes: let the user pick one
2126 console.debug('multiple matching barcodes');
2130 angular.forEach(resp, function(cp) {
2134 'open-ils.circ.copy_details.retrieve',
2135 egCore.auth.token(), cp.id
2136 ).then(function(r) {
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()
2146 return $q.all(promises)
2148 return $uibModal.open({
2149 templateUrl: './circ/share/t_barcode_choice_dialog',
2152 ['$scope', '$uibModalInstance',
2153 function($scope, $uibModalInstance) {
2154 $scope.matches = matches;
2155 $scope.ok = function(barcode) {
2156 $uibModalInstance.close();
2157 final_barcode = barcode;
2159 $scope.cancel = function() {$uibModalInstance.dismiss()}
2161 }).result.then(function() { return final_barcode });
2166 function generate_penalty_dialog_watch_callback($scope,egCore,allPenalties) {
2167 return function(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;
2176 if (penalty_id > 100) {
2177 $scope.args.custom_penalty = penalty_id;
2178 $scope.args.penalty = null;
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();
2184 $scope.args.custom_depth = $scope.args.org.ou_type().depth();
2190 service.create_penalty = function(user_id) {
2191 return $uibModal.open({
2192 templateUrl: './circ/share/t_new_message_dialog',
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;
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;
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;
2216 penalty : 21, // default to Note
2217 org : egCore.org.get(egCore.auth.user().ws_ou())
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();
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) {
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) {
2235 $scope.args.org = egCore.org.get(ctx_org);
2242 allPenalties : service.get_all_penalty_types,
2243 goodOrgs : egCore.perm.hasPermAt('UPDATE_USER', true)
2247 var pen = new egCore.idl.ausp();
2251 message : args.note ? args.note : ''
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);
2259 pen.standing_penalty(args.penalty);
2261 pen.staff(egCore.auth.user().id());
2262 pen.set_date('now');
2264 return egCore.net.request(
2266 'open-ils.actor.user.penalty.apply',
2267 egCore.auth.token(), pen, msg
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',
2279 ['$scope','$uibModalInstance','allPenalties','goodOrgs',
2280 function($scope , $uibModalInstance , allPenalties , goodOrgs) {
2281 // We may need to vivicate usr_penalty (pen) or usr_message (aum)
2283 pen = new egCore.idl.ausp();
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());
2290 aum.ischanged(true);
2293 aum = new egCore.idl.aum();
2294 aum.create_date('now');
2295 aum.sending_lib(pen.org_unit());
2299 pen.ischanged(true);
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;
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;
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;
2319 var penalty_id = pen.standing_penalty();
2321 penalty : pen.isnew()
2322 ? 21 // default to Note
2324 pub : typeof aum.pub() == 'boolean'
2327 title : aum.title(),
2328 note : aum.message() ? aum.message() : '',
2329 org : egCore.org.get(pen.org_unit()),
2330 deleted : typeof aum.deleted() == 'boolean'
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()
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;
2344 if (penalty_id > 100) {
2345 $scope.args.custom_penalty = penalty_id;
2346 $scope.args.penalty = null;
2348 $scope.ok = function(count) { $uibModalInstance.close($scope.args) }
2349 $scope.cancel = function($event) {
2350 $uibModalInstance.dismiss();
2351 $event.preventDefault();
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;
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) {
2366 $scope.args.org = egCore.org.get(ctx_org);
2374 allPenalties : service.get_all_penalty_types,
2375 goodOrgs : egCore.perm.hasPermAt('UPDATE_USER', true)
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);
2388 pen.standing_penalty(args.penalty);
2390 return egCore.net.request(
2392 'open-ils.actor.user.penalty.modify',
2393 egCore.auth.token(), pen, aum