1 # ---------------------------------------------------------------
2 # Copyright (C) 2005 Georgia Public Library Service
3 # Bill Erickson <highfalutin@gmail.com>
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 # ---------------------------------------------------------------
17 package OpenILS::Application::Circ::Holds;
18 use base qw/OpenILS::Application/;
19 use strict; use warnings;
20 use List::Util qw(shuffle);
21 use OpenILS::Application::AppUtils;
24 use OpenSRF::EX qw(:try);
28 use OpenSRF::Utils::Logger qw(:logger);
29 use OpenILS::Utils::CStoreEditor q/:funcs/;
30 use OpenILS::Utils::PermitHold;
31 use OpenSRF::Utils::SettingsClient;
32 use OpenILS::Const qw/:const/;
33 use OpenILS::Application::Circ::Transit;
34 use OpenILS::Application::Actor::Friends;
36 use DateTime::Format::ISO8601;
37 use OpenILS::Utils::DateTime qw/:datetime/;
38 use Digest::MD5 qw(md5_hex);
39 use OpenSRF::Utils::Cache;
40 use OpenSRF::Utils::JSON;
41 my $apputils = "OpenILS::Application::AppUtils";
44 __PACKAGE__->register_method(
45 method => "test_and_create_hold_batch",
46 api_name => "open-ils.circ.holds.test_and_create.batch",
49 desc => q/This is for batch creating a set of holds where every field is identical except for the targets./,
51 { desc => 'Authentication token', type => 'string' },
52 { desc => 'Hash of named parameters. Same as for open-ils.circ.title_hold.is_possible, though the pertinent target field is automatically populated based on the hold_type and the specified list of targets.', type => 'object'},
53 { desc => 'Array of target ids', type => 'array' }
56 desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
61 __PACKAGE__->register_method(
62 method => "test_and_create_hold_batch",
63 api_name => "open-ils.circ.holds.test_and_create.batch.override",
66 desc => '@see open-ils.circ.holds.test_and_create.batch',
71 sub test_and_create_hold_batch {
72 my( $self, $conn, $auth, $params, $target_list, $oargs ) = @_;
75 if ($self->api_name =~ /override/) {
77 $oargs = { all => 1 } unless defined $oargs;
78 $$params{oargs} = $oargs; # for is_possible checking.
81 my $e = new_editor(authtoken=>$auth);
82 return $e->die_event unless $e->checkauth;
83 $$params{'requestor'} = $e->requestor->id;
86 if ($$params{'hold_type'} eq 'T') { $target_field = 'titleid'; }
87 elsif ($$params{'hold_type'} eq 'C') { $target_field = 'copy_id'; }
88 elsif ($$params{'hold_type'} eq 'R') { $target_field = 'copy_id'; }
89 elsif ($$params{'hold_type'} eq 'F') { $target_field = 'copy_id'; }
90 elsif ($$params{'hold_type'} eq 'I') { $target_field = 'issuanceid'; }
91 elsif ($$params{'hold_type'} eq 'V') { $target_field = 'volume_id'; }
92 elsif ($$params{'hold_type'} eq 'M') { $target_field = 'mrid'; }
93 elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
94 else { return undef; }
96 my $formats_map = delete $$params{holdable_formats_map};
98 foreach (@$target_list) {
99 $$params{$target_field} = $_;
101 # copy the requested formats from the target->formats map
102 # into the top-level formats attr for each hold
103 $$params{holdable_formats} = $formats_map->{$_};
106 ($res) = $self->method_lookup(
107 'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
108 if ($res->{'success'} == 1 || ($override && $res->{place_unfillable})) {
110 $params->{'depth'} = $res->{'depth'} if $res->{'depth'};
112 if ($$oargs{honor_user_settings}) {
113 my $recipient = $e->retrieve_actor_user($$params{patronid})
114 or return $e->die_event;
115 my $opac_hold_notify = $e->search_actor_user_setting(
116 {usr => $$params{patronid}, name => 'opac.hold_notify'})->[0];
117 if ($opac_hold_notify) {
118 if ($opac_hold_notify->value =~ 'email') {
119 $$params{email_notify} = 1;
121 if ($opac_hold_notify->value =~ 'phone') {
122 my $opac_default_phone = $e->search_actor_user_setting(
123 {usr => $$params{patronid}, name => 'opac.default_phone'})->[0];
124 # FIXME - what's up with the ->value putting quotes around the string?
125 if ($opac_default_phone && $opac_default_phone->value =~ /^"(.*)"$/) {
126 $$params{phone_notify} = $1;
129 if ($opac_hold_notify->value =~ 'sms') {
130 my $opac_default_sms_carrier = $e->search_actor_user_setting(
131 {usr => $$params{patronid}, name => 'opac.default_sms_carrier'})->[0];
132 $$params{sms_carrier} = $opac_default_sms_carrier->value if $opac_default_sms_carrier;
133 my $opac_default_sms_notify = $e->search_actor_user_setting(
134 {usr => $$params{patronid}, name => 'opac.default_sms_notify'})->[0];
135 if ($opac_default_sms_notify && $opac_default_sms_notify->value =~ /^"(.*)"$/) {
136 $$params{sms_notify} = $1;
142 # Remove oargs from params so holds can be created.
143 if ($$params{oargs}) {
144 delete $$params{oargs};
147 my $ahr = construct_hold_request_object($params);
148 my ($res2) = $self->method_lookup(
150 ? 'open-ils.circ.holds.create.override'
151 : 'open-ils.circ.holds.create'
152 )->run($auth, $ahr, $oargs);
154 'target' => $$params{$target_field},
157 $conn->respond($res2);
160 'target' => $$params{$target_field},
163 $conn->respond($res);
169 __PACKAGE__->register_method(
170 method => "test_and_create_batch_hold_event",
171 api_name => "open-ils.circ.holds.test_and_create.subscription_batch",
174 desc => q/This is for batch creating a set of holds where every field is identical except for the target users./,
176 { desc => 'Authentication token', type => 'string' },
177 { desc => 'Hash of named parameters. Same as for open-ils.circ.title_hold.is_possible, though the pertinent target field is automatically populated based on the hold_type and the specified list of target users.', type => 'object'},
178 { desc => 'Container ID of the user bucket holding target users', type => 'number' },
179 { desc => 'target object ID (clarified by hold_type in param hash)', type => 'number' },
180 { desc => 'Randomize flag, set to 0 for "not randomized"', type => 'bool' }
183 desc => 'Stream of objects structured as {total=>X, count=>Y, target=>Z, patronid=>A, result=>$hold_id} on success, -1 on missing arg, event (or stream of events on "result" key of object) on error(s)',
188 __PACKAGE__->register_method(
189 method => "test_and_create_batch_hold_event",
190 api_name => "open-ils.circ.holds.test_and_create.subscription_batch.override",
193 desc => '@see open-ils.circ.holds.test_and_create.subscription_batch',
198 sub test_and_create_batch_hold_event {
199 my( $self, $conn, $auth, $params, $target_bucket, $target_id, $randomize, $oargs ) = @_;
202 $randomize //= 1; # default to random hold creation order
203 $$params{hold_type} //= 'T'; # default to title holds
206 if ($self->api_name =~ /override/) {
208 $oargs = { all => 1 } unless defined $oargs;
209 $$params{oargs} = $oargs; # for is_possible checking.
212 my $e = new_editor(authtoken=>$auth);
213 return $e->die_event unless $e->checkauth;
214 $$params{'requestor'} = $e->requestor->id;
217 my $org = $e->requestor->ws_ou || $e->requestor->home_ou;
218 # the perm locaiton shouldn't really matter here since holds
219 # will exist all over and MANAGE_HOLD_GROUPS should be universal
220 my $evt = $U->check_perms($e->requestor->id, $org, 'MANAGE_HOLD_GROUPS');
223 my $rand_setting = $U->ou_ancestor_setting_value($org, 'holds.subscription.randomize');
224 $randomize = $rand_setting if (defined $rand_setting);
227 if ($$params{'hold_type'} eq 'T') { $target_field = 'titleid'; }
228 elsif ($$params{'hold_type'} eq 'C') { $target_field = 'copy_id'; }
229 elsif ($$params{'hold_type'} eq 'R') { $target_field = 'copy_id'; }
230 elsif ($$params{'hold_type'} eq 'F') { $target_field = 'copy_id'; }
231 elsif ($$params{'hold_type'} eq 'I') { $target_field = 'issuanceid'; }
232 elsif ($$params{'hold_type'} eq 'V') { $target_field = 'volume_id'; }
233 elsif ($$params{'hold_type'} eq 'M') { $target_field = 'mrid'; }
234 elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
235 else { return undef; }
237 # Check for a valid record.
238 # XXX For now, because only title holds are allowed, we'll add only that check.
239 my $target_check = $e->json_query({
240 select => {bre => ['id']},
242 where => {deleted => 'f', id => $target_id}
244 return {error=>'invalid_target'} if (!@$target_check);
246 my $formats_map = delete($$params{holdable_formats_map}) || {};
248 my $target_list = $e->search_container_user_bucket_item({bucket => $target_bucket});
249 @$target_list = shuffle(@$target_list) if $randomize;
251 # Record the request...
253 my $bhe = Fieldmapper::action::batch_hold_event->new;
255 $bhe->staff($e->requestor->id);
256 $bhe->bucket($target_bucket);
257 $bhe->target($target_id);
258 $bhe->hold_type($$params{hold_type});
259 $bhe = $e->create_action_batch_hold_event($bhe) or return $e->die_event;
262 my $total = scalar(@$target_list);
264 $conn->respond({total => $total, count => $count});
266 my $triggers = OpenSRF::AppSession->connect('open-ils.trigger');
267 foreach (@$target_list) {
269 $$params{$target_field} = $target_id;
270 $$params{patronid} = $_->target_user;
272 my $usr = $e->retrieve_actor_user([
281 my $user_setting_map = {
282 map { $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) }
286 $$params{pickup_lib} = $$user_setting_map{'opac.default_pickup_location'} || $usr->home_ou;
288 if ($user_setting_map->{'opac.hold_notify'} =~ /email/) {
289 $$params{email_notify} = 1;
291 delete $$params{email_notify};
294 if ($user_setting_map->{'opac.default_phone'} && $user_setting_map->{'opac.hold_notify'} =~ /phone/) {
295 $$params{phone_notify} = $user_setting_map->{'opac.default_phone'};
297 delete $$params{phone_notify};
300 if ($user_setting_map->{'opac.default_sms_carrier'}
301 && $user_setting_map->{'opac.default_sms_notify'}
302 && $user_setting_map->{'opac.hold_notify'} =~ /sms/) {
303 $$params{sms_carrier} = $user_setting_map->{'opac.default_sms_carrier'};
304 $$params{sms_notify} = $user_setting_map->{'opac.default_sms_notify'};
306 delete $$params{sms_carrier};
307 delete $$params{sms_notify};
310 # copy the requested formats from the target->formats map
311 # into the top-level formats attr for each hold ... empty for now, T holds only
312 $$params{holdable_formats} = $formats_map->{$_};
315 ($res) = $self->method_lookup(
316 'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
317 if ($res->{'success'} == 1) {
319 $params->{'depth'} = $res->{'depth'} if $res->{'depth'};
321 # Remove oargs from params so holds can be created.
322 if ($$params{oargs}) {
323 delete $$params{oargs};
326 my $ahr = construct_hold_request_object($params);
327 my ($res2) = $self->method_lookup(
329 ? 'open-ils.circ.holds.create.override'
330 : 'open-ils.circ.holds.create'
331 )->run($auth, $ahr, $oargs);
333 total => $total, count => $count,
334 'patronid' => $$params{patronid},
335 'target' => $$params{$target_field},
338 $conn->respond($res2);
340 unless (ref($res2->{result})) { # success returns a hold id only
342 my $bhem = Fieldmapper::action::batch_hold_event_map->new;
344 $bhem->batch_hold_event($bhe->id);
345 $bhem->hold($res2->{result});
346 $e->create_action_batch_hold_event_map($bhem) or return $e->die_event;
349 my $hold = $e->retrieve_action_hold_request($bhem->hold);
350 $triggers->request('open-ils.trigger.event.autocreate', 'hold_request.success', $hold, $hold->pickup_lib);
355 total => $total, count => $count,
356 'target' => $$params{$target_field},
357 'failedpatronid' => $$params{patronid},
360 $conn->respond($res);
367 __PACKAGE__->register_method(
368 method => "rollback_batch_hold_event",
369 api_name => "open-ils.circ.holds.rollback.subscription_batch",
372 desc => q/This is for batch creating a set of holds where every field is identical except for the target users./,
374 { desc => 'Authentication token', type => 'string' },
375 { desc => 'Hold Group Event ID to roll back', type => 'number' },
378 desc => 'Stream of objects structured as {total=>X, count=>Y} on success, event on error',
383 sub rollback_batch_hold_event {
384 my( $self, $conn, $auth, $event_id ) = @_;
386 my $e = new_editor(authtoken=>$auth,xact=>1);
387 return $e->die_event unless $e->checkauth;
389 my $org = $e->requestor->ws_ou || $e->requestor->home_ou;
390 my $evt = $U->check_perms($e->requestor->id, $org, 'MANAGE_HOLD_GROUPS');
393 my $batch_event = $e->retrieve_action_batch_hold_event($event_id);
394 my $target_list = $e->search_action_batch_hold_event_map({batch_hold_event => $event_id});
396 my $total = scalar(@$target_list);
398 $conn->respond({total => $total, count => $count});
400 for my $target (@$target_list) {
402 $self->method_lookup('open-ils.circ.hold.cancel')->run($auth, $target->hold, 8);
403 $conn->respond({ total => $total, count => $count });
406 $batch_event->cancelled('now');
407 $e->update_action_batch_hold_event($batch_event);
412 sub construct_hold_request_object {
415 my $ahr = Fieldmapper::action::hold_request->new;
418 foreach my $field (keys %{ $params }) {
419 if ($field eq 'depth') { $ahr->selection_depth($$params{$field}); }
420 elsif ($field eq 'patronid') {
421 $ahr->usr($$params{$field}); }
422 elsif ($field eq 'titleid') { $ahr->target($$params{$field}); }
423 elsif ($field eq 'copy_id') { $ahr->target($$params{$field}); }
424 elsif ($field eq 'issuanceid') { $ahr->target($$params{$field}); }
425 elsif ($field eq 'volume_id') { $ahr->target($$params{$field}); }
426 elsif ($field eq 'mrid') { $ahr->target($$params{$field}); }
427 elsif ($field eq 'partid') { $ahr->target($$params{$field}); }
429 $ahr->$field($$params{$field});
435 __PACKAGE__->register_method(
436 method => "create_hold_batch",
437 api_name => "open-ils.circ.holds.create.batch",
440 desc => q/@see open-ils.circ.holds.create.batch/,
442 { desc => 'Authentication token', type => 'string' },
443 { desc => 'Array of hold objects', type => 'array' }
446 desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
451 __PACKAGE__->register_method(
452 method => "create_hold_batch",
453 api_name => "open-ils.circ.holds.create.override.batch",
456 desc => '@see open-ils.circ.holds.create.batch',
461 sub create_hold_batch {
462 my( $self, $conn, $auth, $hold_list, $oargs ) = @_;
463 (my $method = $self->api_name) =~ s/\.batch//og;
464 foreach (@$hold_list) {
465 my ($res) = $self->method_lookup($method)->run($auth, $_, $oargs);
466 $conn->respond($res);
472 __PACKAGE__->register_method(
473 method => "create_hold",
474 api_name => "open-ils.circ.holds.create",
476 desc => "Create a new hold for an item. From a permissions perspective, " .
477 "the login session is used as the 'requestor' of the hold. " .
478 "The hold recipient is determined by the 'usr' setting within the hold object. " .
479 'First we verify the requestor has holds request permissions. ' .
480 'Then we verify that the recipient is allowed to make the given hold. ' .
481 'If not, we see if the requestor has "override" capabilities. If not, ' .
482 'a permission exception is returned. If permissions allow, we cycle ' .
483 'through the set of holds objects and create. ' .
484 'If the recipient does not have permission to place multiple holds ' .
485 'on a single title and said operation is attempted, a permission ' .
486 'exception is returned',
488 { desc => 'Authentication token', type => 'string' },
489 { desc => 'Hold object for hold to be created',
490 type => 'object', class => 'ahr' }
493 desc => 'New ahr ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
498 __PACKAGE__->register_method(
499 method => "create_hold",
500 api_name => "open-ils.circ.holds.create.override",
501 notes => '@see open-ils.circ.holds.create',
503 desc => "If the recipient is not allowed to receive the requested hold, " .
504 "call this method to attempt the override",
506 { desc => 'Authentication token', type => 'string' },
508 desc => 'Hold object for hold to be created',
509 type => 'object', class => 'ahr'
513 desc => 'New hold (ahr) ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
519 my( $self, $conn, $auth, $hold, $oargs ) = @_;
520 return -1 unless $hold;
521 my $e = new_editor(authtoken=>$auth, xact=>1);
522 return $e->die_event unless $e->checkauth;
525 if ($self->api_name =~ /override/) {
527 $oargs = { all => 1 } unless defined $oargs;
532 my $requestor = $e->requestor;
533 my $recipient = $requestor;
535 if( $requestor->id ne $hold->usr ) {
536 # Make sure the requestor is allowed to place holds for
537 # the recipient if they are not the same people
538 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->die_event;
539 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->die_event;
542 # If the related org setting tells us to, block if patron privs have expired
543 my $expire_setting = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_BLOCK_HOLD_FOR_EXPIRED_PATRON);
544 if ($expire_setting) {
545 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
546 clean_ISO8601($recipient->expire_date));
548 push( @events, OpenILS::Event->new(
549 'PATRON_ACCOUNT_EXPIRED',
550 "payload" => {"fail_part" => "actor.usr.privs_expired"}
551 )) if( CORE::time > $expire->epoch ) ;
554 # Now make sure the recipient is allowed to receive the specified hold
555 my $porg = $recipient->home_ou;
556 my $rid = $e->requestor->id;
557 my $t = $hold->hold_type;
559 # See if a duplicate hold already exists
561 usr => $recipient->id,
563 fulfillment_time => undef,
564 target => $hold->target,
565 cancel_time => undef,
568 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
570 my $existing = $e->search_action_hold_request($sargs);
572 # See if the requestor has the CREATE_DUPLICATE_HOLDS perm.
573 my $can_dup = $e->allowed('CREATE_DUPLICATE_HOLDS', $recipient->home_ou);
574 # How many are allowed.
575 my $num_dups = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_MAX_DUPLICATE_HOLDS, $e) || 0;
576 push( @events, OpenILS::Event->new('HOLD_EXISTS'))
577 unless (($t eq 'T' || $t eq 'M') && $can_dup && scalar(@$existing) < $num_dups);
578 # Note: We check for @$existing < $num_dups because we're adding a hold with this call.
581 my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
582 push( @events, OpenILS::Event->new('HOLD_ITEM_CHECKED_OUT')) if $checked_out;
584 if ( $t eq OILS_HOLD_TYPE_METARECORD ) {
585 return $e->die_event unless $e->allowed('MR_HOLDS', $porg);
586 } elsif ( $t eq OILS_HOLD_TYPE_TITLE ) {
587 return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
588 } elsif ( $t eq OILS_HOLD_TYPE_VOLUME ) {
589 return $e->die_event unless $e->allowed('VOLUME_HOLDS', $porg);
590 } elsif ( $t eq OILS_HOLD_TYPE_MONOPART ) {
591 return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
592 } elsif ( $t eq OILS_HOLD_TYPE_ISSUANCE ) {
593 return $e->die_event unless $e->allowed('ISSUANCE_HOLDS', $porg);
594 } elsif ( $t eq OILS_HOLD_TYPE_COPY ) {
595 return $e->die_event unless $e->allowed('COPY_HOLDS', $porg);
596 } elsif ( $t eq OILS_HOLD_TYPE_FORCE || $t eq OILS_HOLD_TYPE_RECALL ) {
597 my $copy = $e->retrieve_asset_copy($hold->target)
598 or return $e->die_event;
599 if ( $t eq OILS_HOLD_TYPE_FORCE ) {
600 return $e->die_event unless $e->allowed('COPY_HOLDS_FORCE', $copy->circ_lib);
601 } elsif ( $t eq OILS_HOLD_TYPE_RECALL ) {
602 return $e->die_event unless $e->allowed('COPY_HOLDS_RECALL', $copy->circ_lib);
611 for my $evt (@events) {
613 my $name = $evt->{textcode};
614 if($oargs->{all} || grep { $_ eq $name } @{$oargs->{events}}) {
615 return $e->die_event unless $e->allowed("$name.override", $porg);
623 # Check for hold expiration in the past, and set it to empty string.
624 $hold->expire_time(undef) if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1);
626 # set the configured expire time
627 unless($hold->expire_time || $U->is_true($hold->frozen)) {
628 $hold->expire_time(calculate_expire_time($recipient->home_ou));
632 # if behind-the-desk pickup is supported at the hold pickup lib,
633 # set the value to the patron default, unless a value has already
634 # been applied. If it's not supported, force the value to false.
636 my $bdous = $U->ou_ancestor_setting_value(
638 'circ.holds.behind_desk_pickup_supported', $e);
641 if (!defined $hold->behind_desk) {
643 my $set = $e->search_actor_user_setting({
645 name => 'circ.holds_behind_desk'
648 $hold->behind_desk('t') if $set and
649 OpenSRF::Utils::JSON->JSON2perl($set->value);
652 # behind the desk not supported, force it to false
653 $hold->behind_desk('f');
656 $hold->requestor($e->requestor->id);
657 $hold->request_lib($e->requestor->ws_ou);
658 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
659 $hold = $e->create_action_hold_request($hold) or return $e->die_event;
663 $conn->respond_complete($hold->id);
665 $U->simplereq('open-ils.hold-targeter',
666 'open-ils.hold-targeter.target', {hold => $hold->id}
667 ) unless $U->is_true($hold->frozen);
672 # makes sure that a user has permission to place the type of requested hold
673 # returns the Perm exception if not allowed, returns undef if all is well
674 sub _check_holds_perm {
675 my($type, $user_id, $org_id) = @_;
679 $evt = $apputils->check_perms($user_id, $org_id, "MR_HOLDS" );
680 } elsif ($type eq "T") {
681 $evt = $apputils->check_perms($user_id, $org_id, "TITLE_HOLDS" );
682 } elsif($type eq "V") {
683 $evt = $apputils->check_perms($user_id, $org_id, "VOLUME_HOLDS");
684 } elsif($type eq "C") {
685 $evt = $apputils->check_perms($user_id, $org_id, "COPY_HOLDS" );
692 # tests if the given user is allowed to place holds on another's behalf
693 sub _check_request_holds_perm {
696 if (my $evt = $apputils->check_perms(
697 $user_id, $org_id, "REQUEST_HOLDS")) {
702 my $ses_is_req_note = 'The login session is the requestor. If the requestor is different from the user, ' .
703 'then the requestor must have VIEW_HOLD permissions';
705 __PACKAGE__->register_method(
706 method => "retrieve_holds_by_id",
707 api_name => "open-ils.circ.holds.retrieve_by_id",
709 desc => "Retrieve the hold, with hold transits attached, for the specified ID. $ses_is_req_note",
711 { desc => 'Authentication token', type => 'string' },
712 { desc => 'Hold ID', type => 'number' }
715 desc => 'Hold object with transits attached, event on error',
721 sub retrieve_holds_by_id {
722 my($self, $client, $auth, $hold_id) = @_;
723 my $e = new_editor(authtoken=>$auth);
724 $e->checkauth or return $e->event;
725 $e->allowed('VIEW_HOLD') or return $e->event;
727 my $holds = $e->search_action_hold_request(
729 { id => $hold_id , fulfillment_time => undef },
731 order_by => { ahr => "request_time" },
733 flesh_fields => {ahr => ['notes']}
738 flesh_hold_transits($holds);
739 flesh_hold_notices($holds, $e);
744 __PACKAGE__->register_method(
745 method => "retrieve_holds",
746 api_name => "open-ils.circ.holds.retrieve",
748 desc => "Retrieves all the holds, with hold transits attached, for the specified user. $ses_is_req_note",
750 { desc => 'Authentication token', type => 'string' },
751 { desc => 'User ID', type => 'integer' },
752 { desc => 'Available Only', type => 'boolean' }
755 desc => 'list of holds, event on error',
760 __PACKAGE__->register_method(
761 method => "retrieve_holds",
762 api_name => "open-ils.circ.holds.id_list.retrieve",
765 desc => "Retrieves all the hold IDs, for the specified user. $ses_is_req_note",
767 { desc => 'Authentication token', type => 'string' },
768 { desc => 'User ID', type => 'integer' },
769 { desc => 'Available Only', type => 'boolean' }
772 desc => 'list of holds, event on error',
777 __PACKAGE__->register_method(
778 method => "retrieve_holds",
779 api_name => "open-ils.circ.holds.canceled.retrieve",
782 desc => "Retrieves all the cancelled holds for the specified user. $ses_is_req_note",
784 { desc => 'Authentication token', type => 'string' },
785 { desc => 'User ID', type => 'integer' }
788 desc => 'list of holds, event on error',
793 __PACKAGE__->register_method(
794 method => "retrieve_holds",
795 api_name => "open-ils.circ.holds.canceled.id_list.retrieve",
798 desc => "Retrieves list of cancelled hold IDs for the specified user. $ses_is_req_note",
800 { desc => 'Authentication token', type => 'string' },
801 { desc => 'User ID', type => 'integer' }
804 desc => 'list of hold IDs, event on error',
811 my ($self, $client, $auth, $user_id, $available) = @_;
813 my $e = new_editor(authtoken=>$auth);
814 return $e->event unless $e->checkauth;
815 $user_id = $e->requestor->id unless defined $user_id;
817 my $notes_filter = {staff => 'f'};
818 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
819 unless($user_id == $e->requestor->id) {
820 if($e->allowed('VIEW_HOLD', $user->home_ou)) {
821 $notes_filter = {staff => 't'}
823 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
824 $e, $user_id, $e->requestor->id, 'hold.view');
825 return $e->event unless $allowed;
828 # staff member looking at his/her own holds can see staff and non-staff notes
829 $notes_filter = {} if $e->allowed('VIEW_HOLD', $user->home_ou);
833 select => {ahr => ['id']},
835 where => {usr => $user_id, fulfillment_time => undef}
838 if($self->api_name =~ /canceled/) {
840 # Fetch the canceled holds
841 # order cancelled holds by cancel time, most recent first
843 $holds_query->{order_by} = [{class => 'ahr', field => 'cancel_time', direction => 'desc'}];
846 my $cancel_count = $U->ou_ancestor_setting_value(
847 $e->requestor->ws_ou, 'circ.holds.canceled.display_count', $e);
849 unless($cancel_count) {
850 $cancel_age = $U->ou_ancestor_setting_value(
851 $e->requestor->ws_ou, 'circ.holds.canceled.display_age', $e);
853 # if no settings are defined, default to last 10 cancelled holds
854 $cancel_count = 10 unless $cancel_age;
857 if($cancel_count) { # limit by count
859 $holds_query->{where}->{cancel_time} = {'!=' => undef};
860 $holds_query->{limit} = $cancel_count;
862 } elsif($cancel_age) { # limit by age
864 # find all of the canceled holds that were canceled within the configured time frame
865 my $date = DateTime->now->subtract(seconds => OpenILS::Utils::DateTime->interval_to_seconds($cancel_age));
866 $date = $U->epoch2ISO8601($date->epoch);
867 $holds_query->{where}->{cancel_time} = {'>=' => $date};
872 # order non-cancelled holds by ready-for-pickup, then active, followed by suspended
873 # "compare" sorts false values to the front. testing pickup_lib != current_shelf_lib
874 # will sort by pl = csl > pl != csl > followed by csl is null;
875 $holds_query->{order_by} = [
877 field => 'pickup_lib',
878 compare => {'!=' => {'+ahr' => 'current_shelf_lib'}}},
879 {class => 'ahr', field => 'shelf_time'},
880 {class => 'ahr', field => 'frozen'},
881 {class => 'ahr', field => 'request_time'}
884 $holds_query->{where}->{cancel_time} = undef;
886 $holds_query->{where}->{shelf_time} = {'!=' => undef};
888 $holds_query->{where}->{pickup_lib} = {'=' => {'+ahr' => 'current_shelf_lib'}};
892 my $hold_ids = $e->json_query($holds_query);
893 $hold_ids = [ map { $_->{id} } @$hold_ids ];
895 return $hold_ids if $self->api_name =~ /id_list/;
898 for my $hold_id ( @$hold_ids ) {
900 my $hold = $e->retrieve_action_hold_request($hold_id);
901 $hold->notes($e->search_action_hold_request_note({hold => $hold_id, %$notes_filter}));
904 $e->search_action_hold_transit_copy([
905 {hold => $hold->id, cancel_time => undef},
906 {order_by => {ahtc => 'source_send_time desc'}, limit => 1}])->[0]
916 __PACKAGE__->register_method(
917 method => 'user_hold_count',
918 api_name => 'open-ils.circ.hold.user.count'
921 sub user_hold_count {
922 my ( $self, $conn, $auth, $userid ) = @_;
923 my $e = new_editor( authtoken => $auth );
924 return $e->event unless $e->checkauth;
925 my $patron = $e->retrieve_actor_user($userid)
927 return $e->event unless $e->allowed( 'VIEW_HOLD', $patron->home_ou );
928 return __user_hold_count( $self, $e, $userid );
931 sub __user_hold_count {
932 my ( $self, $e, $userid ) = @_;
933 my $holds = $e->search_action_hold_request(
936 fulfillment_time => undef,
937 cancel_time => undef,
942 return scalar(@$holds);
946 __PACKAGE__->register_method(
947 method => "retrieve_holds_by_pickup_lib",
948 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
950 "Retrieves all the holds, with hold transits attached, for the specified pickup_ou id."
953 __PACKAGE__->register_method(
954 method => "retrieve_holds_by_pickup_lib",
955 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
956 notes => "Retrieves all the hold ids for the specified pickup_ou id. "
959 sub retrieve_holds_by_pickup_lib {
960 my ($self, $client, $login_session, $ou_id) = @_;
962 #FIXME -- put an appropriate permission check here
963 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
964 # $login_session, $user_id, 'VIEW_HOLD' );
965 #return $evt if $evt;
967 my $holds = $apputils->simplereq(
969 "open-ils.cstore.direct.action.hold_request.search.atomic",
971 pickup_lib => $ou_id ,
972 fulfillment_time => undef,
975 { order_by => { ahr => "request_time" } }
978 if ( ! $self->api_name =~ /id_list/ ) {
979 flesh_hold_transits($holds);
983 return [ map { $_->id } @$holds ];
987 __PACKAGE__->register_method(
988 method => "uncancel_hold",
989 api_name => "open-ils.circ.hold.uncancel"
993 my($self, $client, $auth, $hold_id) = @_;
994 my $e = new_editor(authtoken=>$auth, xact=>1);
995 return $e->die_event unless $e->checkauth;
997 my $hold = $e->retrieve_action_hold_request($hold_id)
998 or return $e->die_event;
999 return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
1001 if ($hold->fulfillment_time) {
1005 unless ($hold->cancel_time) {
1010 # if configured to reset the request time, also reset the expire time
1011 if($U->ou_ancestor_setting_value(
1012 $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
1014 $hold->request_time('now');
1015 $hold->expire_time(calculate_expire_time($hold->request_lib));
1018 $hold->clear_cancel_time;
1019 $hold->clear_cancel_cause;
1020 $hold->clear_cancel_note;
1021 $hold->clear_shelf_time;
1022 $hold->clear_current_copy;
1023 $hold->clear_capture_time;
1024 $hold->clear_prev_check_time;
1025 $hold->clear_shelf_expire_time;
1026 $hold->clear_current_shelf_lib;
1028 $e->update_action_hold_request($hold) or return $e->die_event;
1031 $U->simplereq('open-ils.hold-targeter',
1032 'open-ils.hold-targeter.target', {hold => $hold_id});
1038 __PACKAGE__->register_method(
1039 method => "cancel_hold",
1040 api_name => "open-ils.circ.hold.cancel",
1042 desc => 'Cancels the specified hold. The login session is the requestor. If the requestor is different from the usr field ' .
1043 'on the hold, the requestor must have CANCEL_HOLDS permissions. The hold may be either the hold object or the hold id',
1045 {desc => 'Authentication token', type => 'string'},
1046 {desc => 'Hold ID', type => 'number'},
1047 {desc => 'Cause of Cancellation', type => 'string'},
1048 {desc => 'Note', type => 'string'}
1051 desc => '1 on success, event on error'
1057 my($self, $client, $auth, $holdid, $cause, $note) = @_;
1059 my $e = new_editor(authtoken=>$auth, xact=>1);
1060 return $e->die_event unless $e->checkauth;
1062 my $hold = $e->retrieve_action_hold_request($holdid)
1063 or return $e->die_event;
1065 if( $e->requestor->id ne $hold->usr ) {
1066 return $e->die_event unless $e->allowed('CANCEL_HOLDS');
1069 if ($hold->cancel_time) {
1074 # If the hold is captured, reset the copy status
1075 if( $hold->capture_time and $hold->current_copy ) {
1077 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1078 or return $e->die_event;
1080 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1081 $logger->info("canceling hold $holdid whose item is on the holds shelf");
1082 # $logger->info("setting copy to status 'reshelving' on hold cancel");
1083 # $copy->status(OILS_COPY_STATUS_RESHELVING);
1084 # $copy->editor($e->requestor->id);
1085 # $copy->edit_date('now');
1086 # $e->update_asset_copy($copy) or return $e->event;
1088 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1090 my $hid = $hold->id;
1091 $logger->warn("! canceling hold [$hid] that is in transit");
1092 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
1095 my $trans = $e->retrieve_action_transit_copy($transid);
1096 # Leave the transit alive, but set the copy status to
1097 # reshelving so it will be properly reshelved when it gets back home
1099 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
1100 $e->update_action_transit_copy($trans) or return $e->die_event;
1106 $hold->cancel_time('now');
1107 $hold->cancel_cause($cause);
1108 $hold->cancel_note($note);
1109 $e->update_action_hold_request($hold)
1110 or return $e->die_event;
1114 # re-fetch the hold to pick up the real cancel_time (not "now") for A/T
1116 $hold = $e->retrieve_action_hold_request($hold->id) or return $e->die_event;
1119 if ($e->requestor->id == $hold->usr) {
1120 $U->create_events_for_hook('hold_request.cancel.patron', $hold, $hold->pickup_lib);
1122 $U->create_events_for_hook('hold_request.cancel.staff', $hold, $hold->pickup_lib);
1128 my $update_hold_desc = 'The login session is the requestor. ' .
1129 'If the requestor is different from the usr field on the hold, ' .
1130 'the requestor must have UPDATE_HOLDS permissions. ' .
1131 'If supplying a hash of hold data, "id" must be included. ' .
1132 'The hash is ignored if a hold object is supplied, ' .
1133 'so you should supply only one kind of hold data argument.' ;
1135 __PACKAGE__->register_method(
1136 method => "update_hold",
1137 api_name => "open-ils.circ.hold.update",
1139 desc => "Updates the specified hold. $update_hold_desc",
1141 {desc => 'Authentication token', type => 'string'},
1142 {desc => 'Hold Object', type => 'object'},
1143 {desc => 'Hash of values to be applied', type => 'object'}
1146 desc => 'Hold ID on success, event on error',
1152 __PACKAGE__->register_method(
1153 method => "batch_update_hold",
1154 api_name => "open-ils.circ.hold.update.batch",
1157 desc => "Updates the specified hold(s). $update_hold_desc",
1159 {desc => 'Authentication token', type => 'string'},
1160 {desc => 'Array of hold obejcts', type => 'array' },
1161 {desc => 'Array of hashes of values to be applied', type => 'array' }
1164 desc => 'Hold ID per success, event per error',
1170 my($self, $client, $auth, $hold, $values) = @_;
1171 my $e = new_editor(authtoken=>$auth, xact=>1);
1172 return $e->die_event unless $e->checkauth;
1173 my $resp = update_hold_impl($self, $e, $hold, $values);
1174 if ($U->event_code($resp)) {
1178 $e->commit; # FIXME: update_hold_impl already does $e->commit ??
1182 sub batch_update_hold {
1183 my($self, $client, $auth, $hold_list, $values_list) = @_;
1184 my $e = new_editor(authtoken=>$auth);
1185 return $e->die_event unless $e->checkauth;
1187 my $count = ($hold_list) ? scalar(@$hold_list) : scalar(@$values_list); # FIXME: we don't know for sure that we got $values_list. we could have neither list.
1189 $values_list ||= []; # FIXME: either move this above $count declaration, or send an event if both lists undef. Probably the latter.
1191 # FIXME: Failing over to [] guarantees warnings for "Use of unitialized value" in update_hold_impl call.
1192 # FIXME: We should be sure we only call update_hold_impl with hold object OR hash, not both.
1194 for my $idx (0..$count-1) {
1196 my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
1197 $e->xact_commit unless $U->event_code($resp);
1198 $client->respond($resp);
1202 return undef; # not in the register return type, assuming we should always have at least one list populated
1205 sub update_hold_impl {
1206 my($self, $e, $hold, $values) = @_;
1208 my $need_retarget = 0;
1211 $hold = $e->retrieve_action_hold_request($values->{id})
1212 or return $e->die_event;
1213 for my $k (keys %$values) {
1214 # Outside of pickup_lib (covered by the first regex) I don't know when these would currently change.
1215 # But hey, why not cover things that may happen later?
1216 if ($k =~ '_(lib|ou)$' || $k eq 'target' || $k eq 'hold_type' || $k eq 'requestor' || $k eq 'selection_depth' || $k eq 'holdable_formats') {
1217 if (defined $values->{$k} && defined $hold->$k() && $values->{$k} ne $hold->$k()) {
1218 # Value changed? RETARGET!
1220 } elsif (defined $hold->$k() != defined $values->{$k}) {
1221 # Value being set or cleared? RETARGET!
1225 if (defined $values->{$k}) {
1226 $hold->$k($values->{$k});
1228 my $f = "clear_$k"; $hold->$f();
1233 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
1234 or return $e->die_event;
1236 # don't allow the user to be changed
1237 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
1239 if($hold->usr ne $e->requestor->id) {
1240 # if the hold is for a different user, make sure the
1241 # requestor has the appropriate permissions
1242 my $usr = $e->retrieve_actor_user($hold->usr)
1243 or return $e->die_event;
1244 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1248 # --------------------------------------------------------------
1249 # Changing the request time is like playing God
1250 # --------------------------------------------------------------
1251 if($hold->request_time ne $orig_hold->request_time) {
1252 return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
1253 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
1257 # --------------------------------------------------------------
1258 # Code for making sure staff have appropriate permissons for cut_in_line
1259 # This, as is, doesn't prevent a user from cutting their own holds in line
1261 # --------------------------------------------------------------
1262 if($U->is_true($hold->cut_in_line) ne $U->is_true($orig_hold->cut_in_line)) {
1263 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
1267 # --------------------------------------------------------------
1268 # Disallow hold suspencion if the hold is already captured.
1269 # --------------------------------------------------------------
1270 if ($U->is_true($hold->frozen) and not $U->is_true($orig_hold->frozen)) {
1271 $hold_status = _hold_status($e, $hold);
1272 if ($hold_status > 2 && $hold_status != 7) { # hold is captured
1273 $logger->info("bypassing hold freeze on captured hold");
1274 return OpenILS::Event->new('HOLD_SUSPEND_AFTER_CAPTURE');
1279 # --------------------------------------------------------------
1280 # if the hold is on the holds shelf or in transit and the pickup
1281 # lib changes we need to create a new transit.
1282 # --------------------------------------------------------------
1283 if($orig_hold->pickup_lib ne $hold->pickup_lib) {
1285 $hold_status = _hold_status($e, $hold) unless $hold_status;
1287 if($hold_status == 3) { # in transit
1289 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
1290 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
1292 $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
1294 # update the transit to reflect the new pickup location
1295 my $transit = $e->search_action_hold_transit_copy(
1296 {hold=>$hold->id, cancel_time => undef, dest_recv_time => undef})->[0]
1297 or return $e->die_event;
1299 $transit->prev_dest($transit->dest); # mark the previous destination on the transit
1300 $transit->dest($hold->pickup_lib);
1301 $e->update_action_hold_transit_copy($transit) or return $e->die_event;
1303 } elsif($hold_status == 4 or $hold_status == 5 or $hold_status == 8) { # on holds shelf
1305 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
1306 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
1308 $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
1310 if ($hold->pickup_lib eq $orig_hold->current_shelf_lib) {
1311 # This can happen if the pickup lib is changed while the hold is
1312 # on the shelf, then changed back to the original pickup lib.
1313 # Restore the original shelf_expire_time to prevent abuse.
1314 set_hold_shelf_expire_time(undef, $hold, $e, $hold->shelf_time);
1317 # clear to prevent premature shelf expiration
1318 $hold->clear_shelf_expire_time;
1320 # If a copy is targeted and pickup lib changes,
1321 # clear the current_copy so a retarget will re-evaluate
1322 # the hold from scratch.
1323 } elsif ($hold_status == 2) {
1324 $logger->info("Pickup location changed and waiting for capture, clear current_copy for hold ".$hold->id);
1325 $hold->clear_current_copy;
1329 if($U->is_true($hold->frozen)) {
1330 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1331 $hold->clear_current_copy;
1332 $hold->clear_prev_check_time;
1333 # Clear expire_time to prevent frozen holds from expiring.
1334 $logger->info("clearing expire_time for frozen hold ".$hold->id);
1335 $hold->clear_expire_time;
1338 # If the hold_expire_time is in the past && is not equal to the
1339 # original expire_time, then reset the expire time to be in the
1341 if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1 && $U->datecmp($hold->expire_time, $orig_hold->expire_time) != 0) {
1342 $hold->expire_time(calculate_expire_time($hold->request_lib));
1345 # If the hold is reactivated, reset the expire_time.
1346 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1347 $logger->info("Reset expire_time on activated hold ".$hold->id);
1348 $hold->expire_time(calculate_expire_time($hold->request_lib));
1351 $e->update_action_hold_request($hold) or return $e->die_event;
1354 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1355 $logger->info("Running targeter on activated hold ".$hold->id);
1356 $U->simplereq('open-ils.hold-targeter',
1357 'open-ils.hold-targeter.target', {hold => $hold->id});
1360 # a change to mint-condition changes the set of potential copies, so retarget the hold;
1361 if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
1362 _reset_hold($self, $e->requestor, $hold)
1363 } elsif($need_retarget && !defined $hold->capture_time()) { # If needed, retarget the hold due to changes
1364 $U->simplereq('open-ils.hold-targeter',
1365 'open-ils.hold-targeter.target', {hold => $hold->id});
1371 # this does not update the hold in the DB. It only
1372 # sets the shelf_expire_time field on the hold object.
1373 # start_time is optional and defaults to 'now'
1374 sub set_hold_shelf_expire_time {
1375 my ($class, $hold, $editor, $start_time) = @_;
1377 my $shelf_expire = $U->ou_ancestor_setting_value(
1379 'circ.holds.default_shelf_expire_interval',
1383 return undef unless $shelf_expire;
1385 $start_time = ($start_time) ?
1386 DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time)) :
1387 DateTime->now(time_zone => 'local'); # without time_zone we get UTC ... yuck!
1389 my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($shelf_expire);
1390 my $expire_time = $start_time->add(seconds => $seconds);
1392 # if the shelf expire time overlaps with a pickup lib's
1393 # closed date, push it out to the first open date
1394 my $dateinfo = $U->storagereq(
1395 'open-ils.storage.actor.org_unit.closed_date.overlap',
1396 $hold->pickup_lib, $expire_time->strftime('%FT%T%z'));
1399 my $dt_parser = DateTime::Format::ISO8601->new;
1400 $expire_time = $dt_parser->parse_datetime(clean_ISO8601($dateinfo->{end}));
1402 # TODO: enable/disable time bump via setting?
1403 $expire_time->set(hour => '23', minute => '59', second => '59');
1405 $logger->info("circulator: shelf_expire_time overlaps".
1406 " with closed date, pushing expire time to $expire_time");
1409 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
1415 my($e, $orig_hold, $hold, $copy) = @_;
1416 my $src = $orig_hold->pickup_lib;
1417 my $dest = $hold->pickup_lib;
1419 $logger->info("putting hold into transit on pickup_lib update");
1421 my $transit = Fieldmapper::action::hold_transit_copy->new;
1422 $transit->hold($hold->id);
1423 $transit->source($src);
1424 $transit->dest($dest);
1425 $transit->target_copy($copy->id);
1426 $transit->source_send_time('now');
1427 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1429 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1430 $copy->editor($e->requestor->id);
1431 $copy->edit_date('now');
1433 $e->create_action_hold_transit_copy($transit) or return $e->die_event;
1434 $e->update_asset_copy($copy) or return $e->die_event;
1438 # if the hold is frozen, this method ensures that the hold is not "targeted",
1439 # that is, it clears the current_copy and prev_check_time to essentiallly
1440 # reset the hold. If it is being activated, it runs the targeter in the background
1441 sub update_hold_if_frozen {
1442 my($self, $e, $hold, $orig_hold) = @_;
1443 return if $hold->capture_time;
1445 if($U->is_true($hold->frozen)) {
1446 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1447 $hold->clear_current_copy;
1448 $hold->clear_prev_check_time;
1451 if($U->is_true($orig_hold->frozen)) {
1452 $logger->info("Running targeter on activated hold ".$hold->id);
1453 $U->simplereq('open-ils.hold-targeter',
1454 'open-ils.hold-targeter.target', {hold => $hold->id});
1459 __PACKAGE__->register_method(
1460 method => "hold_note_CUD",
1461 api_name => "open-ils.circ.hold_request.note.cud",
1463 desc => 'Create, update or delete a hold request note. If the operator (from Auth. token) '
1464 . 'is not the owner of the hold, the UPDATE_HOLD permission is required',
1466 { desc => 'Authentication token', type => 'string' },
1467 { desc => 'Hold note object', type => 'object' }
1470 desc => 'Returns the note ID, event on error'
1476 my($self, $conn, $auth, $note) = @_;
1478 my $e = new_editor(authtoken => $auth, xact => 1);
1479 return $e->die_event unless $e->checkauth;
1481 my $hold = $e->retrieve_action_hold_request($note->hold)
1482 or return $e->die_event;
1484 if($hold->usr ne $e->requestor->id) {
1485 my $usr = $e->retrieve_actor_user($hold->usr);
1486 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1487 $note->staff('t') if $note->isnew;
1491 $e->create_action_hold_request_note($note) or return $e->die_event;
1492 } elsif($note->ischanged) {
1493 $e->update_action_hold_request_note($note) or return $e->die_event;
1494 } elsif($note->isdeleted) {
1495 $e->delete_action_hold_request_note($note) or return $e->die_event;
1503 __PACKAGE__->register_method(
1504 method => "retrieve_hold_status",
1505 api_name => "open-ils.circ.hold.status.retrieve",
1507 desc => 'Calculates the current status of the hold. The requestor must have ' .
1508 'VIEW_HOLD permissions if the hold is for a user other than the requestor' ,
1510 { desc => 'Hold ID', type => 'number' }
1513 # type => 'number', # event sometimes
1514 desc => <<'END_OF_DESC'
1515 Returns event on error or:
1516 -1 on error (for now),
1517 1 for 'waiting for copy to become available',
1518 2 for 'waiting for copy capture',
1521 5 for 'hold-shelf-delay'
1524 8 for 'captured, on wrong hold shelf'
1531 sub retrieve_hold_status {
1532 my($self, $client, $auth, $hold_id) = @_;
1534 my $e = new_editor(authtoken => $auth);
1535 return $e->event unless $e->checkauth;
1536 my $hold = $e->retrieve_action_hold_request($hold_id)
1537 or return $e->event;
1539 if( $e->requestor->id != $hold->usr ) {
1540 return $e->event unless $e->allowed('VIEW_HOLD');
1543 return _hold_status($e, $hold);
1549 if ($hold->cancel_time) {
1552 if ($U->is_true($hold->frozen) && !$hold->capture_time) {
1555 if ($hold->current_shelf_lib and $hold->current_shelf_lib ne $hold->pickup_lib) {
1558 if ($hold->fulfillment_time) {
1561 return 1 unless $hold->current_copy;
1562 return 2 unless $hold->capture_time;
1564 my $copy = $hold->current_copy;
1565 unless( ref $copy ) {
1566 $copy = $e->retrieve_asset_copy($hold->current_copy)
1567 or return $e->event;
1570 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
1572 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
1574 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
1575 return 4 unless $hs_wait_interval;
1577 # if a hold_shelf_status_delay interval is defined and start_time plus
1578 # the interval is greater than now, consider the hold to be in the virtual
1579 # "on its way to the holds shelf" status. Return 5.
1581 my $transit = $e->search_action_hold_transit_copy({
1583 target_copy => $copy->id,
1584 cancel_time => undef,
1585 dest_recv_time => {'!=' => undef},
1587 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
1588 $start_time = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time));
1589 my $end_time = $start_time->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($hs_wait_interval));
1591 return 5 if $end_time > DateTime->now;
1600 __PACKAGE__->register_method(
1601 method => "retrieve_hold_queue_stats",
1602 api_name => "open-ils.circ.hold.queue_stats.retrieve",
1604 desc => 'Returns summary data about the state of a hold',
1606 { desc => 'Authentication token', type => 'string'},
1607 { desc => 'Hold ID', type => 'number'},
1610 desc => q/Summary object with keys:
1611 total_holds : total holds in queue
1612 queue_position : current queue position
1613 potential_copies : number of potential copies for this hold
1614 estimated_wait : estimated wait time in days
1615 status : hold status
1616 -1 => error or unexpected state,
1617 1 => 'waiting for copy to become available',
1618 2 => 'waiting for copy capture',
1621 5 => 'hold-shelf-delay'
1628 sub retrieve_hold_queue_stats {
1629 my($self, $conn, $auth, $hold_id) = @_;
1630 my $e = new_editor(authtoken => $auth);
1631 return $e->event unless $e->checkauth;
1632 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
1633 if($e->requestor->id != $hold->usr) {
1634 return $e->event unless $e->allowed('VIEW_HOLD');
1636 return retrieve_hold_queue_status_impl($e, $hold);
1639 sub retrieve_hold_queue_status_impl {
1643 # The holds queue is defined as the distinct set of holds that share at
1644 # least one potential copy with the context hold, plus any holds that
1645 # share the same hold type and target. The latter part exists to
1646 # accomodate holds that currently have no potential copies
1647 my $q_holds = $e->json_query({
1649 # fetch cut_in_line and request_time since they're in the order_by
1650 # and we're asking for distinct values
1651 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1655 select => { ahcm => ['hold'] },
1660 'field' => 'target_copy',
1661 'fkey' => 'target_copy'
1665 where => { '+ahcm2' => { hold => $hold->id } },
1672 "field" => "cut_in_line",
1673 "transform" => "coalesce",
1675 "direction" => "desc"
1677 { "class" => "ahr", "field" => "request_time" }
1682 if (!@$q_holds) { # none? maybe we don't have a map ...
1683 $q_holds = $e->json_query({
1684 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1689 "field" => "cut_in_line",
1690 "transform" => "coalesce",
1692 "direction" => "desc"
1694 { "class" => "ahr", "field" => "request_time" }
1697 hold_type => $hold->hold_type,
1698 target => $hold->target,
1699 capture_time => undef,
1700 cancel_time => undef,
1702 {expire_time => undef },
1703 {expire_time => {'>' => 'now'}}
1711 for my $h (@$q_holds) {
1712 last if $h->{id} == $hold->id;
1716 my $hold_data = $e->json_query({
1718 acp => [ {column => 'id', transform => 'count', aggregate => 1, alias => 'count'} ],
1719 ccm => [ {column =>'avg_wait_time'} ]
1725 ccm => {type => 'left'}
1730 where => {'+ahcm' => {hold => $hold->id} }
1733 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1735 my $default_wait = $U->ou_ancestor_setting_value(
1736 $user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL, $e);
1737 my $min_wait = $U->ou_ancestor_setting_value(
1738 $user_org, 'circ.holds.min_estimated_wait_interval', $e);
1739 $min_wait = OpenILS::Utils::DateTime->interval_to_seconds($min_wait || '0 seconds');
1740 $default_wait ||= '0 seconds';
1742 # Estimated wait time is the average wait time across the set
1743 # of potential copies, divided by the number of potential copies
1744 # times the queue position.
1746 my $combined_secs = 0;
1747 my $num_potentials = 0;
1749 for my $wait_data (@$hold_data) {
1750 my $count += $wait_data->{count};
1751 $combined_secs += $count *
1752 OpenILS::Utils::DateTime->interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
1753 $num_potentials += $count;
1756 my $estimated_wait = -1;
1758 if($num_potentials) {
1759 my $avg_wait = $combined_secs / $num_potentials;
1760 $estimated_wait = $qpos * ($avg_wait / $num_potentials);
1761 $estimated_wait = $min_wait if $estimated_wait < $min_wait and $estimated_wait != -1;
1765 total_holds => scalar(@$q_holds),
1766 queue_position => $qpos,
1767 potential_copies => $num_potentials,
1768 status => _hold_status( $e, $hold ),
1769 estimated_wait => int($estimated_wait)
1774 sub fetch_open_hold_by_current_copy {
1777 my $hold = $apputils->simplereq(
1779 'open-ils.cstore.direct.action.hold_request.search.atomic',
1780 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1781 return $hold->[0] if ref($hold);
1785 sub fetch_related_holds {
1788 return $apputils->simplereq(
1790 'open-ils.cstore.direct.action.hold_request.search.atomic',
1791 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1795 __PACKAGE__->register_method(
1796 method => "hold_pull_list",
1797 api_name => "open-ils.circ.hold_pull_list.retrieve",
1799 desc => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1800 'The location is determined by the login session.',
1802 { desc => 'Limit (optional)', type => 'number'},
1803 { desc => 'Offset (optional)', type => 'number'},
1806 desc => 'reference to a list of holds, or event on failure',
1811 __PACKAGE__->register_method(
1812 method => "hold_pull_list",
1813 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
1815 desc => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
1816 'The location is determined by the login session.',
1818 { desc => 'Limit (optional)', type => 'number'},
1819 { desc => 'Offset (optional)', type => 'number'},
1822 desc => 'reference to a list of holds, or event on failure',
1827 __PACKAGE__->register_method(
1828 method => "hold_pull_list",
1829 api_name => "open-ils.circ.hold_pull_list.retrieve.count",
1831 desc => 'Returns a count of holds that need to be "pulled" by a given location. ' .
1832 'The location is determined by the login session.',
1834 { desc => 'Limit (optional)', type => 'number'},
1835 { desc => 'Offset (optional)', type => 'number'},
1838 desc => 'Holds count (integer), or event on failure',
1844 __PACKAGE__->register_method(
1845 method => "hold_pull_list",
1847 # TODO: tag with api_level 2 once fully supported
1848 api_name => "open-ils.circ.hold_pull_list.fleshed.stream",
1850 desc => q/Returns a stream of fleshed holds that need to be
1851 "pulled" by a given location. The location is
1852 determined by the login session.
1853 This API calls always run in authoritative mode./,
1855 { desc => 'Limit (optional)', type => 'number'},
1856 { desc => 'Offset (optional)', type => 'number'},
1859 desc => 'Stream of holds holds, or event on failure',
1864 sub hold_pull_list {
1865 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1866 my( $reqr, $evt ) = $U->checkses($authtoken);
1867 return $evt if $evt;
1869 my $org = $reqr->ws_ou || $reqr->home_ou;
1870 # the perm locaiton shouldn't really matter here since holds
1871 # will exist all over and VIEW_HOLDS should be universal
1872 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1873 return $evt if $evt;
1875 if($self->api_name =~ /count/) {
1877 my $count = $U->storagereq(
1878 'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1879 $org, $limit, $offset );
1881 $logger->info("Grabbing pull list for org unit $org with $count items");
1884 } elsif( $self->api_name =~ /id_list/ ) {
1886 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1887 $org, $limit, $offset );
1889 } elsif ($self->api_name =~ /fleshed/) {
1891 my $ids = $U->storagereq(
1892 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1893 $org, $limit, $offset );
1895 my $e = new_editor(xact => 1, requestor => $reqr);
1896 $conn->respond(uber_hold_impl($e, $_, {flesh_acpl => 1})) for @$ids;
1898 $conn->respond_complete;
1903 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1904 $org, $limit, $offset );
1908 __PACKAGE__->register_method(
1909 method => "print_hold_pull_list",
1910 api_name => "open-ils.circ.hold_pull_list.print",
1912 desc => 'Returns an HTML-formatted holds pull list',
1914 { desc => 'Authtoken', type => 'string'},
1915 { desc => 'Org unit ID. Optional, defaults to workstation org unit', type => 'number'},
1918 desc => 'HTML string',
1924 sub print_hold_pull_list {
1925 my($self, $client, $auth, $org_id) = @_;
1927 my $e = new_editor(authtoken=>$auth);
1928 return $e->event unless $e->checkauth;
1930 $org_id = (defined $org_id) ? $org_id : $e->requestor->ws_ou;
1931 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1933 my $hold_ids = $U->storagereq(
1934 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1937 return undef unless @$hold_ids;
1939 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1941 # Holds will /NOT/ be in order after this ...
1942 my $holds = $e->search_action_hold_request({id => $hold_ids}, {substream => 1});
1943 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1945 # ... so we must resort.
1946 my $hold_map = +{map { $_->id => $_ } @$holds};
1947 my $sorted_holds = [];
1948 push @$sorted_holds, $hold_map->{$_} foreach @$hold_ids;
1950 return $U->fire_object_event(
1951 undef, "ahr.format.pull_list", $sorted_holds,
1952 $org_id, undef, undef, $client
1957 __PACKAGE__->register_method(
1958 method => "print_hold_pull_list_stream",
1960 api_name => "open-ils.circ.hold_pull_list.print.stream",
1962 desc => 'Returns a stream of fleshed holds',
1964 { desc => 'Authtoken', type => 'string'},
1965 { desc => 'Hash of optional param: Org unit ID (defaults to workstation org unit), limit, offset, sort (array of: acplo.position, prefix, call_number, suffix, request_time)',
1970 desc => 'A stream of fleshed holds',
1976 sub print_hold_pull_list_stream {
1977 my($self, $client, $auth, $params) = @_;
1979 my $e = new_editor(authtoken=>$auth);
1980 return $e->die_event unless $e->checkauth;
1982 delete($$params{org_id}) unless (int($$params{org_id}));
1983 delete($$params{limit}) unless (int($$params{limit}));
1984 delete($$params{offset}) unless (int($$params{offset}));
1985 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
1986 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
1987 $$params{chunk_size} ||= 10;
1988 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
1990 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
1991 return $e->die_event unless $e->allowed('VIEW_HOLD', $$params{org_id });
1994 if ($$params{sort} && @{ $$params{sort} }) {
1995 for my $s (@{ $$params{sort} }) {
1996 if ($s eq 'acplo.position') {
1998 "class" => "acplo", "field" => "position",
1999 "transform" => "coalesce", "params" => [999]
2001 } elsif ($s eq 'prefix') {
2002 push @$sort, {"class" => "acnp", "field" => "label_sortkey"};
2003 } elsif ($s eq 'call_number') {
2004 push @$sort, {"class" => "acn", "field" => "label_sortkey"};
2005 } elsif ($s eq 'suffix') {
2006 push @$sort, {"class" => "acns", "field" => "label_sortkey"};
2007 } elsif ($s eq 'request_time') {
2008 push @$sort, {"class" => "ahr", "field" => "request_time"};
2012 push @$sort, {"class" => "ahr", "field" => "request_time"};
2015 my $holds_ids = $e->json_query(
2017 "select" => {"ahr" => ["id"]},
2022 "fkey" => "current_copy",
2024 "circ_lib" => $$params{org_id}, "status" => [0,7]
2029 "fkey" => "call_number",
2043 "fkey" => "circ_lib",
2046 "location" => {"=" => {"+acp" => "location"}}
2055 "capture_time" => undef,
2056 "cancel_time" => undef,
2058 {"expire_time" => undef },
2059 {"expire_time" => {">" => "now"}}
2063 (@$sort ? (order_by => $sort) : ()),
2064 ($$params{limit} ? (limit => $$params{limit}) : ()),
2065 ($$params{offset} ? (offset => $$params{offset}) : ())
2066 }, {"substream" => 1}
2067 ) or return $e->die_event;
2069 $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
2072 for my $hid (@$holds_ids) {
2073 push @chunk, $e->retrieve_action_hold_request([
2077 "ahr" => ["usr", "current_copy"],
2079 "acp" => ["location", "call_number", "parts"],
2080 "acn" => ["record","prefix","suffix"]
2085 if (@chunk >= $$params{chunk_size}) {
2086 $client->respond( \@chunk );
2090 $client->respond_complete( \@chunk ) if (@chunk);
2097 __PACKAGE__->register_method(
2098 method => 'fetch_hold_notify',
2099 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
2102 Returns a list of hold notification objects based on hold id.
2103 @param authtoken The loggin session key
2104 @param holdid The id of the hold whose notifications we want to retrieve
2105 @return An array of hold notification objects, event on error.
2109 sub fetch_hold_notify {
2110 my( $self, $conn, $authtoken, $holdid ) = @_;
2111 my( $requestor, $evt ) = $U->checkses($authtoken);
2112 return $evt if $evt;
2113 my ($hold, $patron);
2114 ($hold, $evt) = $U->fetch_hold($holdid);
2115 return $evt if $evt;
2116 ($patron, $evt) = $U->fetch_user($hold->usr);
2117 return $evt if $evt;
2119 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
2120 return $evt if $evt;
2122 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
2123 return $U->cstorereq(
2124 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
2128 __PACKAGE__->register_method(
2129 method => 'create_hold_notify',
2130 api_name => 'open-ils.circ.hold_notification.create',
2132 Creates a new hold notification object
2133 @param authtoken The login session key
2134 @param notification The hold notification object to create
2135 @return ID of the new object on success, Event on error
2139 sub create_hold_notify {
2140 my( $self, $conn, $auth, $note ) = @_;
2141 my $e = new_editor(authtoken=>$auth, xact=>1);
2142 return $e->die_event unless $e->checkauth;
2144 my $hold = $e->retrieve_action_hold_request($note->hold)
2145 or return $e->die_event;
2146 my $patron = $e->retrieve_actor_user($hold->usr)
2147 or return $e->die_event;
2149 return $e->die_event unless
2150 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
2152 $note->notify_staff($e->requestor->id);
2153 $e->create_action_hold_notification($note) or return $e->die_event;
2158 __PACKAGE__->register_method(
2159 method => 'create_hold_note',
2160 api_name => 'open-ils.circ.hold_note.create',
2162 Creates a new hold request note object
2163 @param authtoken The login session key
2164 @param note The hold note object to create
2165 @return ID of the new object on success, Event on error
2169 sub create_hold_note {
2170 my( $self, $conn, $auth, $note ) = @_;
2171 my $e = new_editor(authtoken=>$auth, xact=>1);
2172 return $e->die_event unless $e->checkauth;
2174 my $hold = $e->retrieve_action_hold_request($note->hold)
2175 or return $e->die_event;
2176 my $patron = $e->retrieve_actor_user($hold->usr)
2177 or return $e->die_event;
2179 return $e->die_event unless
2180 $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
2182 $e->create_action_hold_request_note($note) or return $e->die_event;
2187 __PACKAGE__->register_method(
2188 method => 'reset_hold',
2189 api_name => 'open-ils.circ.hold.reset',
2191 Un-captures and un-targets a hold, essentially returning
2192 it to the state it was in directly after it was placed,
2193 then attempts to re-target the hold
2194 @param authtoken The login session key
2195 @param holdid The id of the hold
2201 my( $self, $conn, $auth, $holdid ) = @_;
2203 my ($hold, $evt) = $U->fetch_hold($holdid);
2204 return $evt if $evt;
2205 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
2206 return $evt if $evt;
2207 $evt = _reset_hold($self, $reqr, $hold);
2208 return $evt if $evt;
2213 __PACKAGE__->register_method(
2214 method => 'reset_hold_batch',
2215 api_name => 'open-ils.circ.hold.reset.batch'
2218 sub reset_hold_batch {
2219 my($self, $conn, $auth, $hold_ids) = @_;
2221 my $e = new_editor(authtoken => $auth);
2222 return $e->event unless $e->checkauth;
2224 for my $hold_id ($hold_ids) {
2226 my $hold = $e->retrieve_action_hold_request(
2227 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
2228 or return $e->event;
2230 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
2231 _reset_hold($self, $e->requestor, $hold);
2239 my ($self, $reqr, $hold) = @_;
2241 my $e = new_editor(xact =>1, requestor => $reqr);
2243 $logger->info("reseting hold ".$hold->id);
2245 my $hid = $hold->id;
2247 if( $hold->capture_time and $hold->current_copy ) {
2249 my $copy = $e->retrieve_asset_copy($hold->current_copy)
2250 or return $e->die_event;
2252 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2253 $logger->info("setting copy to status 'reshelving' on hold retarget");
2254 $copy->status(OILS_COPY_STATUS_RESHELVING);
2255 $copy->editor($e->requestor->id);
2256 $copy->edit_date('now');
2257 $e->update_asset_copy($copy) or return $e->die_event;
2259 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
2261 $logger->warn("! reseting hold [$hid] that is in transit");
2262 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
2265 my $trans = $e->retrieve_action_transit_copy($transid);
2267 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
2268 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
2269 $logger->info("Transit abort completed with result $evt");
2270 unless ("$evt" eq 1) {
2279 $hold->clear_capture_time;
2280 $hold->clear_current_copy;
2281 $hold->clear_shelf_time;
2282 $hold->clear_shelf_expire_time;
2283 $hold->clear_current_shelf_lib;
2285 $e->update_action_hold_request($hold) or return $e->die_event;
2288 $U->simplereq('open-ils.hold-targeter',
2289 'open-ils.hold-targeter.target', {hold => $hold->id});
2295 __PACKAGE__->register_method(
2296 method => 'fetch_open_title_holds',
2297 api_name => 'open-ils.circ.open_holds.retrieve',
2299 Returns a list ids of un-fulfilled holds for a given title id
2300 @param authtoken The login session key
2301 @param id the id of the item whose holds we want to retrieve
2302 @param type The hold type - M, T, I, V, C, F, R
2306 sub fetch_open_title_holds {
2307 my( $self, $conn, $auth, $id, $type, $org ) = @_;
2308 my $e = new_editor( authtoken => $auth );
2309 return $e->event unless $e->checkauth;
2312 $org ||= $e->requestor->ws_ou;
2314 # return $e->search_action_hold_request(
2315 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
2317 # XXX make me return IDs in the future ^--
2318 my $holds = $e->search_action_hold_request(
2321 cancel_time => undef,
2323 fulfillment_time => undef
2327 flesh_hold_transits($holds);
2332 sub flesh_hold_transits {
2334 for my $hold ( @$holds ) {
2336 $apputils->simplereq(
2338 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
2339 { hold => $hold->id, cancel_time => undef },
2340 { order_by => { ahtc => 'id desc' }, limit => 1 }
2346 sub flesh_hold_notices {
2347 my( $holds, $e ) = @_;
2348 $e ||= new_editor();
2350 for my $hold (@$holds) {
2351 my $notices = $e->search_action_hold_notification(
2353 { hold => $hold->id },
2354 { order_by => { anh => 'notify_time desc' } },
2359 $hold->notify_count(scalar(@$notices));
2361 my $n = $e->retrieve_action_hold_notification($$notices[0])
2362 or return $e->event;
2363 $hold->notify_time($n->notify_time);
2369 __PACKAGE__->register_method(
2370 method => 'fetch_captured_holds',
2371 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
2375 Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
2376 @param authtoken The login session key
2377 @param org The org id of the location in question
2378 @param match_copy A specific copy to limit to
2382 __PACKAGE__->register_method(
2383 method => 'fetch_captured_holds',
2384 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
2388 Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
2389 @param authtoken The login session key
2390 @param org The org id of the location in question
2391 @param match_copy A specific copy to limit to
2395 __PACKAGE__->register_method(
2396 method => 'fetch_captured_holds',
2397 api_name => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
2401 Returns list ids of shelf-expired un-fulfilled holds for a given title id
2402 @param authtoken The login session key
2403 @param org The org id of the location in question
2404 @param match_copy A specific copy to limit to
2408 __PACKAGE__->register_method(
2409 method => 'fetch_captured_holds',
2411 'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
2415 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2416 for a given shelf lib
2420 __PACKAGE__->register_method(
2421 method => 'fetch_captured_holds',
2423 'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
2427 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2428 for a given shelf lib
2433 sub fetch_captured_holds {
2434 my( $self, $conn, $auth, $org, $match_copy ) = @_;
2436 my $e = new_editor(authtoken => $auth);
2437 return $e->die_event unless $e->checkauth;
2438 return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
2440 $org ||= $e->requestor->ws_ou;
2442 my $current_copy = { '!=' => undef };
2443 $current_copy = { '=' => $match_copy } if $match_copy;
2446 select => { alhr => ['id'] },
2451 fkey => 'current_copy'
2456 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
2458 capture_time => { "!=" => undef },
2459 current_copy => $current_copy,
2460 fulfillment_time => undef,
2461 current_shelf_lib => $org
2465 if($self->api_name =~ /expired/) {
2466 $query->{'where'}->{'+alhr'}->{'-or'} = {
2467 shelf_expire_time => { '<' => 'today'},
2468 cancel_time => { '!=' => undef },
2471 my $hold_ids = $e->json_query( $query );
2473 if ($self->api_name =~ /wrong_shelf/) {
2474 # fetch holds whose current_shelf_lib is $org, but whose pickup
2475 # lib is some other org unit. Ignore already-retrieved holds.
2477 pickup_lib_changed_on_shelf_holds(
2478 $e, $org, [map {$_->{id}} @$hold_ids]);
2479 # match the layout of other items in $hold_ids
2480 push (@$hold_ids, {id => $_}) for @$wrong_shelf;
2484 for my $hold_id (@$hold_ids) {
2485 if($self->api_name =~ /id_list/) {
2486 $conn->respond($hold_id->{id});
2490 $e->retrieve_action_hold_request([
2494 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
2495 order_by => {anh => 'notify_time desc'}
2505 __PACKAGE__->register_method(
2506 method => "print_expired_holds_stream",
2507 api_name => "open-ils.circ.captured_holds.expired.print.stream",
2511 sub print_expired_holds_stream {
2512 my ($self, $client, $auth, $params) = @_;
2514 # No need to check specific permissions: we're going to call another method
2515 # that will do that.
2516 my $e = new_editor("authtoken" => $auth);
2517 return $e->die_event unless $e->checkauth;
2519 delete($$params{org_id}) unless (int($$params{org_id}));
2520 delete($$params{limit}) unless (int($$params{limit}));
2521 delete($$params{offset}) unless (int($$params{offset}));
2522 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2523 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2524 $$params{chunk_size} ||= 10;
2525 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2527 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2529 my @hold_ids = $self->method_lookup(
2530 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
2531 )->run($auth, $params->{"org_id"});
2536 } elsif (defined $U->event_code($hold_ids[0])) {
2538 return $hold_ids[0];
2541 $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
2544 my @hid_chunk = splice @hold_ids, 0, $params->{"chunk_size"};
2546 my $result_chunk = $e->json_query({
2548 "acp" => ["barcode"],
2550 first_given_name second_given_name family_name alias
2559 "field" => "id", "fkey" => "current_copy",
2562 "field" => "id", "fkey" => "call_number",
2565 "field" => "id", "fkey" => "record"
2569 "acpl" => {"field" => "id", "fkey" => "location"}
2572 "au" => {"field" => "id", "fkey" => "usr"}
2575 "where" => {"+ahr" => {"id" => \@hid_chunk}}
2576 }) or return $e->die_event;
2577 $client->respond($result_chunk);
2584 __PACKAGE__->register_method(
2585 method => "check_title_hold_batch",
2586 api_name => "open-ils.circ.title_hold.is_possible.batch",
2589 desc => '@see open-ils.circ.title_hold.is_possible.batch',
2591 { desc => 'Authentication token', type => 'string'},
2592 { desc => 'Array of Hash of named parameters', type => 'array'},
2595 desc => 'Array of response objects',
2601 sub check_title_hold_batch {
2602 my($self, $client, $authtoken, $param_list, $oargs) = @_;
2603 foreach (@$param_list) {
2604 my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
2605 $client->respond($res);
2611 __PACKAGE__->register_method(
2612 method => "check_title_hold",
2613 api_name => "open-ils.circ.title_hold.is_possible",
2615 desc => 'Determines if a hold were to be placed by a given user, ' .
2616 'whether or not said hold would have any potential copies to fulfill it.' .
2617 'The named paramaters of the second argument include: ' .
2618 'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
2619 'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
2621 { desc => 'Authentication token', type => 'string'},
2622 { desc => 'Hash of named parameters', type => 'object'},
2625 desc => 'List of new message IDs (empty if none)',
2631 =head3 check_title_hold (token, hash)
2633 The named fields in the hash are:
2635 patronid - ID of the hold recipient (required)
2636 depth - hold range depth (default 0)
2637 pickup_lib - destination for hold, fallback value for selection_ou
2638 selection_ou - ID of org_unit establishing hard and soft hold boundary settings
2639 issuanceid - ID of the issuance to be held, required for Issuance level hold
2640 partid - ID of the monograph part to be held, required for monograph part level hold
2641 titleid - ID (BRN) of the title to be held, required for Title level hold
2642 volume_id - required for Volume level hold
2643 copy_id - required for Copy level hold
2644 mrid - required for Meta-record level hold
2645 hold_type - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record (default "T")
2647 All key/value pairs are passed on to do_possibility_checks.
2651 # FIXME: better params checking. what other params are required, if any?
2652 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
2653 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
2654 # used in conditionals, where it may be undefined, causing a warning.
2655 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
2657 sub check_title_hold {
2658 my( $self, $client, $authtoken, $params ) = @_;
2659 my $e = new_editor(authtoken=>$authtoken);
2660 return $e->event unless $e->checkauth;
2662 my %params = %$params;
2663 my $depth = $params{depth} || 0;
2664 $params{depth} = $depth; #define $params{depth} if unset, since it gets used later
2665 my $selection_ou = $params{selection_ou} || $params{pickup_lib};
2666 my $oargs = $params{oargs} || {};
2668 if($oargs->{events}) {
2669 @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
2673 my $patron = $e->retrieve_actor_user($params{patronid})
2674 or return $e->event;
2676 if( $e->requestor->id ne $patron->id ) {
2677 return $e->event unless
2678 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
2681 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
2683 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
2684 or return $e->event;
2686 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
2687 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
2690 my $return_depth = $hard_boundary; # default depth to return on success
2691 if(defined $soft_boundary and $depth < $soft_boundary) {
2692 # work up the tree and as soon as we find a potential copy, use that depth
2693 # also, make sure we don't go past the hard boundary if it exists
2695 # our min boundary is the greater of user-specified boundary or hard boundary
2696 my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
2697 $hard_boundary : $depth;
2699 my $depth = $soft_boundary;
2700 while($depth >= $min_depth) {
2701 $logger->info("performing hold possibility check with soft boundary $depth");
2702 @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
2704 $return_depth = $depth;
2709 } elsif(defined $hard_boundary and $depth < $hard_boundary) {
2710 # there is no soft boundary, enforce the hard boundary if it exists
2711 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
2712 @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
2714 # no boundaries defined, fall back to user specifed boundary or no boundary
2715 $logger->info("performing hold possibility check with no boundary");
2716 @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
2719 my $place_unfillable = 0;
2720 $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
2725 "depth" => $return_depth,
2726 "local_avail" => $status[1]
2728 } elsif ($status[2]) {
2729 my $n = scalar @{$status[2]};
2730 return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2732 return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2738 sub do_possibility_checks {
2739 my($e, $patron, $request_lib, $depth, %params) = @_;
2741 my $issuanceid = $params{issuanceid} || "";
2742 my $partid = $params{partid} || "";
2743 my $titleid = $params{titleid} || "";
2744 my $volid = $params{volume_id};
2745 my $copyid = $params{copy_id};
2746 my $mrid = $params{mrid} || "";
2747 my $pickup_lib = $params{pickup_lib};
2748 my $hold_type = $params{hold_type} || 'T';
2749 my $selection_ou = $params{selection_ou} || $pickup_lib;
2750 my $holdable_formats = $params{holdable_formats};
2751 my $oargs = $params{oargs} || {};
2758 if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
2760 return $e->event unless $copy = $e->retrieve_asset_copy($copyid);
2761 return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
2762 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2764 return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
2765 return verify_copy_for_hold(
2766 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
2769 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2771 return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
2772 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2774 return _check_volume_hold_is_possible(
2775 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2778 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
2780 return _check_title_hold_is_possible(
2781 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
2784 } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
2786 return _check_issuance_hold_is_possible(
2787 $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2790 } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
2792 return _check_monopart_hold_is_possible(
2793 $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2796 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2798 # pasing undef as the depth to filtered_records causes the depth
2799 # of the selection_ou to be used, which is not what we want here.
2802 my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
2804 for my $rec (@$recs) {
2805 @status = _check_title_hold_is_possible(
2806 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
2812 # else { Unrecognized hold_type ! } # FIXME: return error? or 0?
2815 sub MR_filter_records {
2822 my $opac_visible = shift;
2824 my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
2825 return $U->storagereq(
2826 'open-ils.storage.metarecord.filtered_records.atomic',
2827 $m, $f, $org_at_depth, $opac_visible
2830 __PACKAGE__->register_method(
2831 method => 'MR_filter_records',
2832 api_name => 'open-ils.circ.holds.metarecord.filtered_records',
2837 sub create_ranged_org_filter {
2838 my($e, $selection_ou, $depth) = @_;
2840 # find the orgs from which this hold may be fulfilled,
2841 # based on the selection_ou and depth
2843 my $top_org = $e->search_actor_org_unit([
2844 {parent_ou => undef},
2845 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
2848 return () if $depth == $top_org->ou_type->depth;
2850 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
2851 %org_filter = (circ_lib => []);
2852 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
2854 $logger->info("hold org filter at depth $depth and selection_ou ".
2855 "$selection_ou created list of @{$org_filter{circ_lib}}");
2861 sub _check_title_hold_is_possible {
2862 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
2863 # $holdable_formats is now unused. We pre-filter the MR's records.
2865 my $e = new_editor();
2866 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2868 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2869 my $copies = $e->json_query(
2871 select => { acp => ['id', 'circ_lib'] },
2876 fkey => 'call_number',
2877 filter => { record => $titleid }
2881 filter => { holdable => 't', deleted => 'f' },
2884 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' },
2885 acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2889 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2890 '+acpm' => { target_copy => undef } # ignore part-linked copies
2895 $logger->info("title possible found ".scalar(@$copies)." potential copies");
2899 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2900 "payload" => {"fail_part" => "no_ultimate_items"}
2905 # -----------------------------------------------------------------------
2906 # sort the copies into buckets based on their circ_lib proximity to
2907 # the patron's home_ou.
2908 # -----------------------------------------------------------------------
2910 my $home_org = $patron->home_ou;
2911 my $req_org = $request_lib->id;
2913 $prox_cache{$home_org} =
2914 $e->search_actor_org_unit_proximity({from_org => $home_org})
2915 unless $prox_cache{$home_org};
2916 my $home_prox = $prox_cache{$home_org};
2917 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2920 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2921 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2923 my @keys = sort { $a <=> $b } keys %buckets;
2926 if( $home_org ne $req_org ) {
2927 # -----------------------------------------------------------------------
2928 # shove the copies close to the request_lib into the primary buckets
2929 # directly before the farthest away copies. That way, they are not
2930 # given priority, but they are checked before the farthest copies.
2931 # -----------------------------------------------------------------------
2932 $prox_cache{$req_org} =
2933 $e->search_actor_org_unit_proximity({from_org => $req_org})
2934 unless $prox_cache{$req_org};
2935 my $req_prox = $prox_cache{$req_org};
2938 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2939 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2941 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2942 my $new_key = $highest_key - 0.5; # right before the farthest prox
2943 my @keys2 = sort { $a <=> $b } keys %buckets2;
2944 for my $key (@keys2) {
2945 last if $key >= $highest_key;
2946 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2950 @keys = sort { $a <=> $b } keys %buckets;
2955 my $age_protect_only = 0;
2956 OUTER: for my $key (@keys) {
2957 my @cps = @{$buckets{$key}};
2959 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2961 for my $copyid (@cps) {
2963 next if $seen{$copyid};
2964 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2965 my $copy = $e->retrieve_asset_copy($copyid);
2966 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2968 unless($title) { # grab the title if we don't already have it
2969 my $vol = $e->retrieve_asset_call_number(
2970 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2971 $title = $vol->record;
2974 @status = verify_copy_for_hold(
2975 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2977 $age_protect_only ||= $status[3];
2978 last OUTER if $status[0];
2982 $status[3] = $age_protect_only;
2986 sub _check_issuance_hold_is_possible {
2987 my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2989 my $e = new_editor();
2990 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2992 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2993 my $copies = $e->json_query(
2995 select => { acp => ['id', 'circ_lib'] },
3001 filter => { issuance => $issuanceid }
3005 filter => { holdable => 't', deleted => 'f' },
3008 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3012 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3018 $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
3022 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3023 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3028 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3029 "payload" => {"fail_part" => "no_ultimate_items"}
3037 # -----------------------------------------------------------------------
3038 # sort the copies into buckets based on their circ_lib proximity to
3039 # the patron's home_ou.
3040 # -----------------------------------------------------------------------
3042 my $home_org = $patron->home_ou;
3043 my $req_org = $request_lib->id;
3045 $prox_cache{$home_org} =
3046 $e->search_actor_org_unit_proximity({from_org => $home_org})
3047 unless $prox_cache{$home_org};
3048 my $home_prox = $prox_cache{$home_org};
3049 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3052 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3053 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3055 my @keys = sort { $a <=> $b } keys %buckets;
3058 if( $home_org ne $req_org ) {
3059 # -----------------------------------------------------------------------
3060 # shove the copies close to the request_lib into the primary buckets
3061 # directly before the farthest away copies. That way, they are not
3062 # given priority, but they are checked before the farthest copies.
3063 # -----------------------------------------------------------------------
3064 $prox_cache{$req_org} =
3065 $e->search_actor_org_unit_proximity({from_org => $req_org})
3066 unless $prox_cache{$req_org};
3067 my $req_prox = $prox_cache{$req_org};
3070 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3071 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3073 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
3074 my $new_key = $highest_key - 0.5; # right before the farthest prox
3075 my @keys2 = sort { $a <=> $b } keys %buckets2;
3076 for my $key (@keys2) {
3077 last if $key >= $highest_key;
3078 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3082 @keys = sort { $a <=> $b } keys %buckets;
3087 my $age_protect_only = 0;
3088 OUTER: for my $key (@keys) {
3089 my @cps = @{$buckets{$key}};
3091 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3093 for my $copyid (@cps) {
3095 next if $seen{$copyid};
3096 $seen{$copyid} = 1; # there could be dupes given the merged buckets
3097 my $copy = $e->retrieve_asset_copy($copyid);
3098 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3100 unless($title) { # grab the title if we don't already have it
3101 my $vol = $e->retrieve_asset_call_number(
3102 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3103 $title = $vol->record;
3106 @status = verify_copy_for_hold(
3107 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3109 $age_protect_only ||= $status[3];
3110 last OUTER if $status[0];
3115 if (!defined($empty_ok)) {
3116 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3117 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3120 return (1,0) if ($empty_ok);
3122 $status[3] = $age_protect_only;
3126 sub _check_monopart_hold_is_possible {
3127 my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3129 my $e = new_editor();
3130 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
3132 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
3133 my $copies = $e->json_query(
3135 select => { acp => ['id', 'circ_lib'] },
3139 field => 'target_copy',
3141 filter => { part => $partid }
3145 filter => { holdable => 't', deleted => 'f' },
3148 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3152 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3158 $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
3162 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3163 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3168 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3169 "payload" => {"fail_part" => "no_ultimate_items"}
3177 # -----------------------------------------------------------------------
3178 # sort the copies into buckets based on their circ_lib proximity to
3179 # the patron's home_ou.
3180 # -----------------------------------------------------------------------
3182 my $home_org = $patron->home_ou;
3183 my $req_org = $request_lib->id;
3185 $prox_cache{$home_org} =
3186 $e->search_actor_org_unit_proximity({from_org => $home_org})
3187 unless $prox_cache{$home_org};
3188 my $home_prox = $prox_cache{$home_org};
3189 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3192 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3193 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3195 my @keys = sort { $a <=> $b } keys %buckets;
3198 if( $home_org ne $req_org ) {
3199 # -----------------------------------------------------------------------
3200 # shove the copies close to the request_lib into the primary buckets
3201 # directly before the farthest away copies. That way, they are not
3202 # given priority, but they are checked before the farthest copies.
3203 # -----------------------------------------------------------------------
3204 $prox_cache{$req_org} =
3205 $e->search_actor_org_unit_proximity({from_org => $req_org})
3206 unless $prox_cache{$req_org};
3207 my $req_prox = $prox_cache{$req_org};
3210 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3211 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3213 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
3214 my $new_key = $highest_key - 0.5; # right before the farthest prox
3215 my @keys2 = sort { $a <=> $b } keys %buckets2;
3216 for my $key (@keys2) {
3217 last if $key >= $highest_key;
3218 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3222 @keys = sort { $a <=> $b } keys %buckets;
3227 my $age_protect_only = 0;
3228 OUTER: for my $key (@keys) {
3229 my @cps = @{$buckets{$key}};
3231 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3233 for my $copyid (@cps) {
3235 next if $seen{$copyid};
3236 $seen{$copyid} = 1; # there could be dupes given the merged buckets
3237 my $copy = $e->retrieve_asset_copy($copyid);
3238 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3240 unless($title) { # grab the title if we don't already have it
3241 my $vol = $e->retrieve_asset_call_number(
3242 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3243 $title = $vol->record;
3246 @status = verify_copy_for_hold(
3247 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3249 $age_protect_only ||= $status[3];
3250 last OUTER if $status[0];
3255 if (!defined($empty_ok)) {
3256 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3257 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3260 return (1,0) if ($empty_ok);
3262 $status[3] = $age_protect_only;
3267 sub _check_volume_hold_is_possible {
3268 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3269 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
3270 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
3271 $logger->info("checking possibility of volume hold for volume ".$vol->id);
3273 my $filter_copies = [];
3274 for my $copy (@$copies) {
3275 # ignore part-mapped copies for regular volume level holds
3276 push(@$filter_copies, $copy) unless
3277 new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
3279 $copies = $filter_copies;
3284 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3285 "payload" => {"fail_part" => "no_ultimate_items"}
3291 my $age_protect_only = 0;
3292 for my $copy ( @$copies ) {
3293 @status = verify_copy_for_hold(
3294 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
3295 $age_protect_only ||= $status[3];
3298 $status[3] = $age_protect_only;
3304 sub verify_copy_for_hold {
3305 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
3306 # $oargs should be undef unless we're overriding.
3307 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
3308 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3311 requestor => $requestor,
3314 title_descriptor => $title->fixed_fields,
3315 pickup_lib => $pickup_lib,
3316 request_lib => $request_lib,
3318 show_event_list => 1
3322 # Check for override permissions on events.
3323 if ($oargs && $permitted && scalar @$permitted) {
3324 # Remove the events from permitted that we can override.
3325 if ($oargs->{events}) {
3326 foreach my $evt (@{$oargs->{events}}) {
3327 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3330 # Now, we handle the override all case by checking remaining
3331 # events against override permissions.
3332 if (scalar @$permitted && $oargs->{all}) {
3333 # Pre-set events and failed members of oargs to empty
3334 # arrays, if they are not set, yet.
3335 $oargs->{events} = [] unless ($oargs->{events});
3336 $oargs->{failed} = [] unless ($oargs->{failed});
3337 # When we're done with these checks, we swap permitted
3338 # with a reference to @disallowed.
3339 my @disallowed = ();
3340 foreach my $evt (@{$permitted}) {
3341 # Check if we've already seen the event in this
3342 # session and it failed.
3343 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3344 push(@disallowed, $evt);
3346 # We have to check if the requestor has the
3347 # override permission.
3349 # AppUtils::check_user_perms returns the perm if
3350 # the user doesn't have it, undef if they do.
3351 if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3352 push(@disallowed, $evt);
3353 push(@{$oargs->{failed}}, $evt->{textcode});
3355 push(@{$oargs->{events}}, $evt->{textcode});
3359 $permitted = \@disallowed;
3363 my $age_protect_only = 0;
3364 if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3365 $age_protect_only = 1;
3369 (not scalar @$permitted), # true if permitted is an empty arrayref
3370 ( # XXX This test is of very dubious value; someone should figure
3371 # out what if anything is checking this value
3372 ($copy->circ_lib == $pickup_lib) and
3373 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3382 sub find_nearest_permitted_hold {
3385 my $editor = shift; # CStoreEditor object
3386 my $copy = shift; # copy to target
3387 my $user = shift; # staff
3388 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3390 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3392 my $bc = $copy->barcode;
3394 # find any existing holds that already target this copy
3395 my $old_holds = $editor->search_action_hold_request(
3396 { current_copy => $copy->id,
3397 cancel_time => undef,
3398 capture_time => undef
3402 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3404 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3405 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3407 my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3409 # the nearest_hold API call now needs this
3410 $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3411 unless ref $copy->call_number;
3413 # search for what should be the best holds for this copy to fulfill
3414 my $best_holds = $U->storagereq(
3415 "open-ils.storage.action.hold_request.nearest_hold.atomic",
3416 $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3418 # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3420 for my $holdid (@$old_holds) {
3421 next unless $holdid;
3422 push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3426 unless(@$best_holds) {
3427 $logger->info("circulator: no suitable holds found for copy $bc");
3428 return (undef, $evt);
3434 # for each potential hold, we have to run the permit script
3435 # to make sure the hold is actually permitted.
3438 for my $holdid (@$best_holds) {
3439 next unless $holdid;
3440 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3442 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3443 # Force and recall holds bypass all rules
3444 if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3448 my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3449 my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3451 $reqr_cache{$hold->requestor} = $reqr;
3452 $org_cache{$hold->request_lib} = $rlib;
3454 # see if this hold is permitted
3455 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3457 patron_id => $hold->usr,
3460 pickup_lib => $hold->pickup_lib,
3461 request_lib => $rlib,
3473 unless( $best_hold ) { # no "good" permitted holds were found
3475 $logger->info("circulator: no suitable holds found for copy $bc");
3476 return (undef, $evt);
3479 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3481 # indicate a permitted hold was found
3482 return $best_hold if $check_only;
3484 # we've found a permitted hold. we need to "grab" the copy
3485 # to prevent re-targeted holds (next part) from re-grabbing the copy
3486 $best_hold->current_copy($copy->id);
3487 $editor->update_action_hold_request($best_hold)
3488 or return (undef, $editor->event);
3493 # re-target any other holds that already target this copy
3494 for my $old_hold (@$old_holds) {
3495 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3496 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3497 $old_hold->id." after a better hold [".$best_hold->id."] was found");
3498 $old_hold->clear_current_copy;
3499 $old_hold->clear_prev_check_time;
3500 $editor->update_action_hold_request($old_hold)
3501 or return (undef, $editor->event);
3502 push(@retarget, $old_hold->id);
3505 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3513 __PACKAGE__->register_method(
3514 method => 'all_rec_holds',
3515 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3519 my( $self, $conn, $auth, $title_id, $args ) = @_;
3521 my $e = new_editor(authtoken=>$auth);
3522 $e->checkauth or return $e->event;
3523 $e->allowed('VIEW_HOLD') or return $e->event;
3526 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
3527 $args->{cancel_time} = undef;
3530 metarecord_holds => []
3532 , volume_holds => []
3534 , recall_holds => []
3537 , issuance_holds => []
3540 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3542 $resp->{metarecord_holds} = $e->search_action_hold_request(
3543 { hold_type => OILS_HOLD_TYPE_METARECORD,
3544 target => $mr_map->metarecord,
3550 $resp->{title_holds} = $e->search_action_hold_request(
3552 hold_type => OILS_HOLD_TYPE_TITLE,
3553 target => $title_id,
3557 my $parts = $e->search_biblio_monograph_part(
3563 $resp->{part_holds} = $e->search_action_hold_request(
3565 hold_type => OILS_HOLD_TYPE_MONOPART,
3571 my $subs = $e->search_serial_subscription(
3572 { record_entry => $title_id }, {idlist=>1});
3575 my $issuances = $e->search_serial_issuance(
3576 {subscription => $subs}, {idlist=>1}
3580 $resp->{issuance_holds} = $e->search_action_hold_request(
3582 hold_type => OILS_HOLD_TYPE_ISSUANCE,
3583 target => $issuances,
3590 my $vols = $e->search_asset_call_number(
3591 { record => $title_id, deleted => 'f' }, {idlist=>1});
3593 return $resp unless @$vols;
3595 $resp->{volume_holds} = $e->search_action_hold_request(
3597 hold_type => OILS_HOLD_TYPE_VOLUME,
3602 my $copies = $e->search_asset_copy(
3603 { call_number => $vols, deleted => 'f' }, {idlist=>1});
3605 return $resp unless @$copies;
3607 $resp->{copy_holds} = $e->search_action_hold_request(
3609 hold_type => OILS_HOLD_TYPE_COPY,
3614 $resp->{recall_holds} = $e->search_action_hold_request(
3616 hold_type => OILS_HOLD_TYPE_RECALL,
3621 $resp->{force_holds} = $e->search_action_hold_request(
3623 hold_type => OILS_HOLD_TYPE_FORCE,
3631 __PACKAGE__->register_method(
3632 method => 'stream_wide_holds',
3635 api_name => 'open-ils.circ.hold.wide_hash.stream'
3638 sub stream_wide_holds {
3639 my($self, $client, $auth, $restrictions, $order_by, $limit, $offset) = @_;
3641 my $e = new_editor(authtoken=>$auth);
3642 $e->checkauth or return $e->event;
3643 $e->allowed('VIEW_HOLD') or return $e->event;
3645 my $st = OpenSRF::AppSession->create('open-ils.storage');
3646 my $req = $st->request(
3647 'open-ils.storage.action.live_holds.wide_hash',
3648 $restrictions, $order_by, $limit, $offset
3651 my $count = $req->recv;
3656 if(UNIVERSAL::isa($count,"Error")) {
3657 throw $count ($count->stringify);
3660 $count = $count->content;
3662 # Force immediate send of count response
3663 my $mbc = $client->max_bundle_count;
3664 $client->max_bundle_count(1);
3665 $client->respond($count);
3666 $client->max_bundle_count($mbc);
3668 while (my $hold = $req->recv) {
3669 $client->respond($hold->content) if $hold->content;
3672 $client->respond_complete;
3678 __PACKAGE__->register_method(
3679 method => 'uber_hold',
3681 api_name => 'open-ils.circ.hold.details.retrieve'
3685 my($self, $client, $auth, $hold_id, $args) = @_;
3686 my $e = new_editor(authtoken=>$auth);
3687 $e->checkauth or return $e->event;
3688 return uber_hold_impl($e, $hold_id, $args);
3691 __PACKAGE__->register_method(
3692 method => 'batch_uber_hold',
3695 api_name => 'open-ils.circ.hold.details.batch.retrieve'
3698 sub batch_uber_hold {
3699 my($self, $client, $auth, $hold_ids, $args) = @_;
3700 my $e = new_editor(authtoken=>$auth);
3701 $e->checkauth or return $e->event;
3702 $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3706 sub uber_hold_impl {
3707 my($e, $hold_id, $args) = @_;
3710 my $flesh_fields = ['current_copy', 'usr', 'notes'];
3711 push (@$flesh_fields, 'requestor') if $args->{include_requestor};
3712 push (@$flesh_fields, 'cancel_cause') if $args->{include_cancel_cause};
3713 push (@$flesh_fields, 'sms_carrier') if $args->{include_sms_carrier};
3715 my $hold = $e->retrieve_action_hold_request([
3717 {flesh => 1, flesh_fields => {ahr => $flesh_fields}}
3718 ]) or return $e->event;
3720 if($hold->usr->id ne $e->requestor->id) {
3721 # caller is asking for someone else's hold
3722 $e->allowed('VIEW_HOLD') or return $e->event;
3723 $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3724 [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3727 # caller is asking for own hold, but may not have permission to view staff notes
3728 unless($e->allowed('VIEW_HOLD')) {
3729 $hold->notes( # filter out any staff notes (unless marked as public)
3730 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3734 my $user = $hold->usr;
3735 $hold->usr($user->id);
3738 my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args);
3740 flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3741 flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3743 my $details = retrieve_hold_queue_status_impl($e, $hold);
3744 $hold->usr($user) if $args->{include_usr}; # re-flesh
3749 ($copy ? (copy => $copy) : ()),
3750 ($volume ? (volume => $volume) : ()),
3751 ($issuance ? (issuance => $issuance) : ()),
3752 ($part ? (part => $part) : ()),
3753 ($args->{include_bre} ? (bre => $bre) : ()),
3754 ($args->{suppress_mvr} ? () : (mvr => $mvr)),
3758 $resp->{copy}->location(
3759 $e->retrieve_asset_copy_location($resp->{copy}->location))
3760 if $resp->{copy} and $args->{flesh_acpl};
3762 unless($args->{suppress_patron_details}) {
3763 my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3764 $resp->{patron_first} = $user->first_given_name,
3765 $resp->{patron_last} = $user->family_name,
3766 $resp->{patron_barcode} = $card->barcode,
3767 $resp->{patron_alias} = $user->alias,
3775 # -----------------------------------------------------
3776 # Returns the MVR object that represents what the
3778 # -----------------------------------------------------
3780 my( $e, $hold, $args ) = @_;
3788 my $no_mvr = $args->{suppress_mvr};
3790 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3791 $metarecord = $e->retrieve_metabib_metarecord($hold->target)
3792 or return $e->event;
3793 $tid = $metarecord->master_record;
3795 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3796 $tid = $hold->target;
3798 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3799 $volume = $e->retrieve_asset_call_number($hold->target)
3800 or return $e->event;
3801 $tid = $volume->record;
3803 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3804 $issuance = $e->retrieve_serial_issuance([
3806 {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3807 ]) or return $e->event;
3809 $tid = $issuance->subscription->record_entry;
3811 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3812 $part = $e->retrieve_biblio_monograph_part([
3814 ]) or return $e->event;
3816 $tid = $part->record;
3818 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY || $hold->hold_type eq OILS_HOLD_TYPE_RECALL || $hold->hold_type eq OILS_HOLD_TYPE_FORCE ) {
3819 $copy = $e->retrieve_asset_copy([
3821 {flesh => 1, flesh_fields => {acp => ['call_number']}}
3822 ]) or return $e->event;
3824 $volume = $copy->call_number;
3825 $tid = $volume->record;
3828 if(!$copy and ref $hold->current_copy ) {
3829 $copy = $hold->current_copy;
3830 $hold->current_copy($copy->id) unless $args->{include_current_copy};
3833 if(!$volume and $copy) {
3834 $volume = $e->retrieve_asset_call_number($copy->call_number);
3837 # TODO return metarcord mvr for M holds
3838 my $title = $e->retrieve_biblio_record_entry($tid);
3839 return ( ($no_mvr) ? undef : $U->record_to_mvr($title),
3840 $volume, $copy, $issuance, $part, $title, $metarecord);
3843 __PACKAGE__->register_method(
3844 method => 'clear_shelf_cache',
3845 api_name => 'open-ils.circ.hold.clear_shelf.get_cache',
3849 Returns the holds processed with the given cache key
3854 sub clear_shelf_cache {
3855 my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3856 my $e = new_editor(authtoken => $auth, xact => 1);
3857 return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3860 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3862 my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3865 $logger->info("no hold data found in cache"); # XXX TODO return event
3871 foreach (keys %$hold_data) {
3872 $maximum += scalar(@{ $hold_data->{$_} });
3874 $client->respond({"maximum" => $maximum, "progress" => 0});
3876 for my $action (sort keys %$hold_data) {
3877 while (@{$hold_data->{$action}}) {
3878 my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3880 my $result_chunk = $e->json_query({
3882 "acp" => ["barcode"],
3884 first_given_name second_given_name family_name alias
3887 "acnp" => [{column => "label", alias => "prefix"}],
3888 "acns" => [{column => "label", alias => "suffix"}],
3896 "field" => "id", "fkey" => "current_copy",
3899 "field" => "id", "fkey" => "call_number",
3902 "field" => "id", "fkey" => "record"
3905 "field" => "id", "fkey" => "prefix"
3908 "field" => "id", "fkey" => "suffix"
3912 "acpl" => {"field" => "id", "fkey" => "location"}
3915 "au" => {"field" => "id", "fkey" => "usr"}
3918 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3919 }, {"substream" => 1}) or return $e->die_event;
3923 +{"action" => $action, "hold_details" => $_}
3934 __PACKAGE__->register_method(
3935 method => 'clear_shelf_process',
3937 api_name => 'open-ils.circ.hold.clear_shelf.process',
3940 1. Find all holds that have expired on the holds shelf
3942 3. If a clear-shelf status is configured, put targeted copies into this status
3943 4. Divide copies into 3 groups: items to transit, items to reshelve, and items
3944 that are needed for holds. No subsequent action is taken on the holds
3945 or items after grouping.
3950 sub clear_shelf_process {
3951 my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
3953 my $e = new_editor(authtoken=>$auth);
3954 $e->checkauth or return $e->die_event;
3955 my $cache = OpenSRF::Utils::Cache->new('global');
3957 $org_id ||= $e->requestor->ws_ou;
3958 $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
3960 my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
3962 my @hold_ids = $self->method_lookup(
3963 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
3964 )->run($auth, $org_id, $match_copy);
3969 my @canceled_holds; # newly canceled holds
3970 $chunk_size ||= 25; # chunked status updates
3971 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3974 for my $hold_id (@hold_ids) {
3976 $logger->info("Clear shelf processing hold $hold_id");
3978 my $hold = $e->retrieve_action_hold_request([
3981 flesh_fields => {ahr => ['current_copy']}
3985 if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
3986 $hold->cancel_time('now');
3987 $hold->cancel_cause(2); # Hold Shelf expiration
3988 $e->update_action_hold_request($hold) or return $e->die_event;
3989 push(@canceled_holds, $hold_id);
3992 my $copy = $hold->current_copy;
3994 if($copy_status or $copy_status == 0) {
3995 # if a clear-shelf copy status is defined, update the copy
3996 $copy->status($copy_status);
3997 $copy->edit_date('now');
3998 $copy->editor($e->requestor->id);
3999 $e->update_asset_copy($copy) or return $e->die_event;
4002 push(@holds, $hold);
4003 $client->respond({maximum => int(scalar(@holds)), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
4012 pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
4015 for my $hold (@holds) {
4017 my $copy = $hold->current_copy;
4018 my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
4020 if($alt_hold and !$match_copy) {
4022 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
4024 } elsif($copy->circ_lib != $e->requestor->ws_ou) {
4026 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
4030 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
4034 my $cache_key = md5_hex(time . $$ . rand());
4035 $logger->info("clear_shelf_cache: storing under $cache_key");
4036 $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours. configurable?
4038 # tell the client we're done
4039 $client->respond_complete({cache_key => $cache_key});
4042 # fire off the hold cancelation trigger and wait for response so don't flood the service
4044 # refetch the holds to pick up the caclulated cancel_time,
4045 # which may be needed by Action/Trigger
4047 my $updated_holds = [];
4048 $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
4051 $U->create_events_for_hook(
4052 'hold_request.cancel.expire_holds_shelf',
4053 $_, $org_id, undef, undef, 1) for @$updated_holds;
4056 # tell the client we're done
4057 $client->respond_complete;
4061 # returns IDs for holds that are on the holds shelf but
4062 # have had their pickup_libs change while on the shelf.
4063 sub pickup_lib_changed_on_shelf_holds {
4066 my $ignore_holds = shift;
4067 $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
4070 select => { alhr => ['id'] },
4075 fkey => 'current_copy'
4080 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
4082 capture_time => { "!=" => undef },
4083 fulfillment_time => undef,
4084 current_shelf_lib => $org_id,
4085 pickup_lib => {'!=' => {'+alhr' => 'current_shelf_lib'}}
4090 $query->{where}->{'+alhr'}->{id} =
4091 {'not in' => $ignore_holds} if @$ignore_holds;
4093 my $hold_ids = $e->json_query($query);
4094 return [ map { $_->{id} } @$hold_ids ];
4097 __PACKAGE__->register_method(
4098 method => 'usr_hold_summary',
4099 api_name => 'open-ils.circ.holds.user_summary',
4101 Returns a summary of holds statuses for a given user
4105 sub usr_hold_summary {
4106 my($self, $conn, $auth, $user_id) = @_;
4108 my $e = new_editor(authtoken=>$auth);
4109 $e->checkauth or return $e->event;
4110 $e->allowed('VIEW_HOLD') or return $e->event;
4112 my $holds = $e->search_action_hold_request(
4115 fulfillment_time => undef,
4116 cancel_time => undef,
4120 my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
4121 $summary{_hold_status($e, $_)} += 1 for @$holds;
4127 __PACKAGE__->register_method(
4128 method => 'hold_has_copy_at',
4129 api_name => 'open-ils.circ.hold.has_copy_at',
4132 'Returns the ID of the found copy and name of the shelving location if there is ' .
4133 'an available copy at the specified org unit. Returns empty hash otherwise. ' .
4134 'The anticipated use for this method is to determine whether an item is ' .
4135 'available at the library where the user is placing the hold (or, alternatively, '.
4136 'at the pickup library) to encourage bypassing the hold placement and just ' .
4137 'checking out the item.' ,
4139 { desc => 'Authentication Token', type => 'string' },
4140 { desc => 'Method Arguments. Options include: hold_type, hold_target, org_unit. '
4141 . 'hold_type is the hold type code (T, V, C, M, ...). '
4142 . 'hold_target is the identifier of the hold target object. '
4143 . 'org_unit is org unit ID.',
4148 desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
4154 sub hold_has_copy_at {
4155 my($self, $conn, $auth, $args) = @_;
4157 my $e = new_editor(authtoken=>$auth);
4158 $e->checkauth or return $e->event;
4160 my $hold_type = $$args{hold_type};
4161 my $hold_target = $$args{hold_target};
4162 my $org_unit = $$args{org_unit};
4165 select => {acp => ['id'], acpl => ['name']},
4170 filter => { holdable => 't', deleted => 'f' },
4173 ccs => {field => 'id', filter => { holdable => 't'}, fkey => 'status' }
4176 where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit, status => [0,7]}},
4180 if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
4182 $query->{where}->{'+acp'}->{id} = $hold_target;
4184 } elsif($hold_type eq 'V') {
4186 $query->{where}->{'+acp'}->{call_number} = $hold_target;
4188 } elsif($hold_type eq 'P') {
4190 $query->{from}->{acp}->{acpm} = {
4191 field => 'target_copy',
4193 filter => {part => $hold_target},
4196 } elsif($hold_type eq 'I') {
4198 $query->{from}->{acp}->{sitem} = {
4201 filter => {issuance => $hold_target},
4204 } elsif($hold_type eq 'T') {
4206 $query->{from}->{acp}->{acn} = {
4208 fkey => 'call_number',
4212 filter => {id => $hold_target},
4220 $query->{from}->{acp}->{acn} = {
4222 fkey => 'call_number',
4231 filter => {metarecord => $hold_target},
4239 my $res = $e->json_query($query)->[0] or return {};
4240 return {copy => $res->{id}, location => $res->{name}} if $res;
4244 # returns true if the user already has an item checked out
4245 # that could be used to fulfill the requested hold.
4246 sub hold_item_is_checked_out {
4247 my($e, $user_id, $hold_type, $hold_target) = @_;
4250 select => {acp => ['id']},
4251 from => {acp => {}},
4255 in => { # copies for circs the user has checked out
4256 select => {circ => ['target_copy']},
4260 checkin_time => undef,
4262 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
4263 {stop_fines => undef}
4273 if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
4275 $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
4277 } elsif($hold_type eq 'V') {
4279 $query->{where}->{'+acp'}->{call_number} = $hold_target;
4281 } elsif($hold_type eq 'P') {
4283 $query->{from}->{acp}->{acpm} = {
4284 field => 'target_copy',
4286 filter => {part => $hold_target},
4289 } elsif($hold_type eq 'I') {
4291 $query->{from}->{acp}->{sitem} = {
4294 filter => {issuance => $hold_target},
4297 } elsif($hold_type eq 'T') {
4299 $query->{from}->{acp}->{acn} = {
4301 fkey => 'call_number',
4305 filter => {id => $hold_target},
4313 $query->{from}->{acp}->{acn} = {
4315 fkey => 'call_number',
4324 filter => {metarecord => $hold_target},
4332 return $e->json_query($query)->[0];
4335 __PACKAGE__->register_method(
4336 method => 'change_hold_title',
4337 api_name => 'open-ils.circ.hold.change_title',
4340 Updates all title level holds targeting the specified bibs to point a new bib./,
4342 { desc => 'Authentication Token', type => 'string' },
4343 { desc => 'New Target Bib Id', type => 'number' },
4344 { desc => 'Old Target Bib Ids', type => 'array' },
4346 return => { desc => '1 on success' }
4350 __PACKAGE__->register_method(
4351 method => 'change_hold_title_for_specific_holds',
4352 api_name => 'open-ils.circ.hold.change_title.specific_holds',
4355 Updates specified holds to target new bib./,
4357 { desc => 'Authentication Token', type => 'string' },
4358 { desc => 'New Target Bib Id', type => 'number' },
4359 { desc => 'Holds Ids for holds to update', type => 'array' },
4361 return => { desc => '1 on success' }
4366 sub change_hold_title {
4367 my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4369 my $e = new_editor(authtoken=>$auth, xact=>1);
4370 return $e->die_event unless $e->checkauth;
4372 my $holds = $e->search_action_hold_request(
4375 capture_time => undef,
4376 cancel_time => undef,
4377 fulfillment_time => undef,
4383 flesh_fields => { ahr => ['usr'] }
4389 for my $hold (@$holds) {
4390 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4391 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4392 $hold->target( $new_bib_id );
4393 $e->update_action_hold_request($hold) or return $e->die_event;
4398 _reset_hold($self, $e->requestor, $_) for @$holds;
4403 sub change_hold_title_for_specific_holds {
4404 my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4406 my $e = new_editor(authtoken=>$auth, xact=>1);
4407 return $e->die_event unless $e->checkauth;
4409 my $holds = $e->search_action_hold_request(
4412 capture_time => undef,
4413 cancel_time => undef,
4414 fulfillment_time => undef,
4420 flesh_fields => { ahr => ['usr'] }
4426 for my $hold (@$holds) {
4427 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4428 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4429 $hold->target( $new_bib_id );
4430 $e->update_action_hold_request($hold) or return $e->die_event;
4435 _reset_hold($self, $e->requestor, $_) for @$holds;
4440 __PACKAGE__->register_method(
4441 method => 'rec_hold_count',
4442 api_name => 'open-ils.circ.bre.holds.count',
4444 desc => q/Returns the total number of holds that target the
4445 selected bib record or its associated copies and call_numbers/,
4447 { desc => 'Bib ID', type => 'number' },
4448 { desc => q/Optional arguments. Supported arguments include:
4449 "pickup_lib_descendant" -> limit holds to those whose pickup
4450 library is equal to or is a child of the provided org unit/,
4454 return => {desc => 'Hold count', type => 'number'}
4458 __PACKAGE__->register_method(
4459 method => 'rec_hold_count',
4460 api_name => 'open-ils.circ.mmr.holds.count',
4462 desc => q/Returns the total number of holds that target the
4463 selected metarecord or its associated copies, call_numbers, and bib records/,
4465 { desc => 'Metarecord ID', type => 'number' },
4467 return => {desc => 'Hold count', type => 'number'}
4471 # XXX Need to add type I holds to these counts
4472 sub rec_hold_count {
4473 my($self, $conn, $target_id, $args) = @_;
4480 filter => {metarecord => $target_id}
4487 filter => { id => $target_id },
4492 if($self->api_name =~ /mmr/) {
4493 delete $bre_join->{bre}->{filter};
4494 $bre_join->{bre}->{join} = $mmr_join;
4500 fkey => 'call_number',
4506 select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4510 cancel_time => undef,
4511 fulfillment_time => undef,
4515 hold_type => [qw/C F R/],
4518 select => {acp => ['id']},
4519 from => { acp => $cn_join }
4529 select => {acn => ['id']},
4530 from => {acn => $bre_join}
4540 select => {bmp => ['id']},
4541 from => {bmp => $bre_join}
4549 target => $target_id
4557 if($self->api_name =~ /mmr/) {
4558 $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4563 select => {bre => ['id']},
4564 from => {bre => $mmr_join}
4570 $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4573 target => $target_id
4579 if (my $pld = $args->{pickup_lib_descendant}) {
4581 my $top_ou = new_editor()->search_actor_org_unit(
4582 {parent_ou => undef}
4583 )->[0]; # XXX Assumes single root node. Not alone in this...
4585 $query->{where}->{'+ahr'}->{pickup_lib} = {
4587 select => {aou => [{
4589 transform => 'actor.org_unit_descendants',
4590 result_field => 'id'
4593 where => {id => $pld}
4595 } if ($pld != $top_ou->id);
4598 # To avoid Internal Server Errors, we get an editor, then run the
4599 # query and check the result. If anything fails, we'll return 0.
4601 if (my $e = new_editor()) {
4602 my $query_result = $e->json_query($query);
4603 if ($query_result && @{$query_result}) {
4604 $result = $query_result->[0]->{count}
4611 # A helper function to calculate a hold's expiration time at a given
4612 # org_unit. Takes the org_unit as an argument and returns either the
4613 # hold expire time as an ISO8601 string or undef if there is no hold
4614 # expiration interval set for the subject ou.
4615 sub calculate_expire_time
4618 my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4620 my $date = DateTime->now->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($interval));
4621 return $U->epoch2ISO8601($date->epoch);
4627 __PACKAGE__->register_method(
4628 method => 'mr_hold_filter_attrs',
4629 api_name => 'open-ils.circ.mmr.holds.filters',
4634 Returns the set of available formats and languages for the
4635 constituent records of the provided metarcord.
4636 If an array of hold IDs is also provided, information about
4637 each is returned as well. This information includes:
4638 1. a slightly easier to read version of holdable_formats
4639 2. attributes describing the set of format icons included
4640 in the set of desired, constituent records.
4643 {desc => 'Metarecord ID', type => 'number'},
4644 {desc => 'Context Org ID', type => 'number'},
4645 {desc => 'Hold ID List', type => 'array'},
4649 Stream of objects. The first will have a 'metarecord' key
4650 containing non-hold-specific metarecord information, subsequent
4651 responses will contain a 'hold' key containing hold-specific
4659 sub mr_hold_filter_attrs {
4660 my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4661 my $e = new_editor();
4663 # by default, return MR / hold attributes for all constituent
4664 # records with holdable copies. If there is a hard boundary,
4665 # though, limit to records with copies within the boundary,
4666 # since anything outside the boundary can never be held.
4669 $org_depth = $U->ou_ancestor_setting_value(
4670 $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4673 # get all org-scoped records w/ holdable copies for this metarecord
4674 my ($bre_ids) = $self->method_lookup(
4675 'open-ils.circ.holds.metarecord.filtered_records')->run(
4676 $mr_id, undef, $org_id, $org_depth);
4678 my $item_lang_attr = 'item_lang'; # configurable?
4679 my $format_attr = $e->retrieve_config_global_flag(
4680 'opac.metarecord.holds.format_attr')->value;
4682 # helper sub for fetching ccvms for a batch of record IDs
4683 sub get_batch_ccvms {
4684 my ($e, $attr, $bre_ids) = @_;
4685 return [] unless $bre_ids and @$bre_ids;
4686 my $vals = $e->search_metabib_record_attr_flat({
4690 return [] unless @$vals;
4691 return $e->search_config_coded_value_map({
4693 code => [map {$_->value} @$vals]
4697 my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4698 my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4703 formats => $formats,
4708 return unless $hold_ids;
4709 my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4710 $icon_attr = $icon_attr ? $icon_attr->value : '';
4712 for my $hold_id (@$hold_ids) {
4713 my $hold = $e->retrieve_action_hold_request($hold_id)
4714 or return $e->event;
4716 next unless $hold->hold_type eq 'M';
4726 # collect the ccvm's for the selected formats / language
4727 # (i.e. the holdable formats) on the MR.
4728 # this assumes a two-key structure for format / language,
4729 # though no assumption is made about the keys themselves.
4730 my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4732 my $format_vals = [];
4733 for my $val (values %$hformats) {
4734 # val is either a single ccvm or an array of them
4735 $val = [$val] unless ref $val eq 'ARRAY';
4736 for my $node (@$val) {
4737 push (@$lang_vals, $node->{_val})
4738 if $node->{_attr} eq $item_lang_attr;
4739 push (@$format_vals, $node->{_val})
4740 if $node->{_attr} eq $format_attr;
4744 # fetch the ccvm's for consistency with the {metarecord} blob
4745 $resp->{hold}{formats} = $e->search_config_coded_value_map({
4746 ctype => $format_attr, code => $format_vals});
4747 $resp->{hold}{langs} = $e->search_config_coded_value_map({
4748 ctype => $item_lang_attr, code => $lang_vals});
4750 # find all of the bib records within this metarcord whose
4751 # format / language match the holdable formats on the hold
4752 my ($bre_ids) = $self->method_lookup(
4753 'open-ils.circ.holds.metarecord.filtered_records')->run(
4754 $hold->target, $hold->holdable_formats,
4755 $hold->selection_ou, $hold->selection_depth);
4757 # now find all of the 'icon' attributes for the records
4758 $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4759 $client->respond($resp);
4765 __PACKAGE__->register_method(
4766 method => "copy_has_holds_count",
4767 api_name => "open-ils.circ.copy.has_holds_count",
4771 Returns the number of holds a paticular copy has
4774 { desc => 'Authentication Token', type => 'string'},
4775 { desc => 'Copy ID', type => 'number'}
4786 sub copy_has_holds_count {
4787 my( $self, $conn, $auth, $copyid ) = @_;
4788 my $e = new_editor(authtoken=>$auth);
4789 return $e->event unless $e->checkauth;
4791 if( $copyid && $copyid > 0 ) {
4792 my $meth = 'retrieve_action_has_holds_count';
4793 my $data = $e->$meth($copyid);
4795 return $data->count();
4801 __PACKAGE__->register_method(
4802 method => "retrieve_holds_by_usr_notify_value_staff",
4803 api_name => "open-ils.circ.holds.retrieve_by_notify_staff",
4805 desc => "Retrieve the hold, for the specified user using the notify value. $ses_is_req_note",
4807 { desc => 'Authentication token', type => 'string' },
4808 { desc => 'User ID', type => 'number' },
4809 { desc => 'notify value', type => 'string' },
4810 { desc => 'notify_type', type => 'string' }
4813 desc => 'Hold objects with transits attached, event on error',
4818 sub retrieve_holds_by_usr_notify_value_staff {
4820 my($self, $conn, $auth, $usr_id, $contact, $cType) = @_;
4822 my $e = new_editor(authtoken=>$auth);
4823 $e->checkauth or return $e->event;
4825 if ($e->requestor->id != $usr_id){
4826 $e->allowed('VIEW_HOLD') or return $e->event;
4830 "select" => { "ahr" => ["id", "sms_notify", "phone_notify", "email_notify", "sms_carrier"]},
4834 "capture_time" => undef,
4835 "cancel_time" => undef,
4836 "fulfillment_time" => undef,
4840 if ($cType eq "day_phone" or $cType eq "evening_phone" or
4841 $cType eq "other_phone" or $cType eq "default_phone"){
4842 $q->{where}->{"-not"} = [
4843 { "phone_notify" => { "=" => $contact} },
4844 { "phone_notify" => { "<>" => undef } }
4849 if ($cType eq "default_sms") {
4850 $q->{where}->{"-not"} = [
4851 { "sms_notify" => { "=" => $contact} },
4852 { "sms_notify" => { "<>" => undef } }
4856 if ($cType eq "default_sms_carrier_id") {
4857 $q->{where}->{"-not"} = [
4858 { "sms_carrier" => { "=" => int($contact)} },
4859 { "sms_carrier" => { "<>" => undef } }
4863 if ($cType =~ /notify/){
4864 # this is was notification pref change
4865 # we find all unfulfilled holds that match have that pref
4866 my $optr = $contact == 1 ? "<>" : "="; # unless it's email, true val means we want to query for not null
4867 my $conj = $optr eq '=' ? '-or' : '-and';
4868 if ($cType =~ /sms/) {
4869 $q->{where}->{$conj} = [ { sms_notify => { $optr => undef } }, { sms_notify => { $optr => '' } } ];
4871 if ($cType =~ /phone/) {
4872 $q->{where}->{$conj} = [ { phone_notify => { $optr => undef } }, { phone_notify => { $optr => '' } } ];
4874 if ($cType =~ /email/) {
4876 $q->{where}->{'+ahr'} = 'email_notify';
4878 $q->{where}->{'-not'} = {'+ahr' => 'email_notify'};
4883 my $holds = $e->json_query($q);
4884 #$hold_ids = [ map { $_->{id} } @$hold_ids ];
4889 __PACKAGE__->register_method(
4890 method => "batch_update_holds_by_value_staff",
4891 api_name => "open-ils.circ.holds.batch_update_holds_by_notify_staff",
4893 desc => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
4895 { desc => 'Authentication token', type => 'string' },
4896 { desc => 'User ID', type => 'number' },
4897 { desc => 'Hold IDs', type => 'array' },
4898 { desc => 'old notify value', type => 'string' },
4899 { desc => 'new notify value', type => 'string' },
4900 { desc => 'field name', type => 'string' },
4901 { desc => 'SMS carrier ID', type => 'number' }
4905 desc => 'Hold objects with transits attached, event on error',
4910 sub batch_update_holds_by_value_staff {
4911 my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType, $carrierId) = @_;
4913 my $e = new_editor(authtoken=>$auth, xact=>1);
4914 $e->checkauth or return $e->event;
4915 if ($e->requestor->id != $usr_id){
4916 $e->allowed('UPDATE_HOLD') or return $e->event;
4920 for my $id (@$hold_ids) {
4922 my $hold = $e->retrieve_action_hold_request($id);
4924 if ($cType eq "day_phone" or $cType eq "evening_phone" or
4925 $cType eq "other_phone" or $cType eq "default_phone") {
4927 if ($newval eq '') {
4928 $hold->clear_phone_notify();
4931 $hold->phone_notify($newval);
4935 if ($cType eq "default_sms"){
4936 if ($newval eq '') {
4937 $hold->clear_sms_notify();
4938 $hold->clear_sms_carrier(); # TODO: prevent orphan sms_carrier, via db trigger
4941 $hold->sms_notify($newval);
4942 $hold->sms_carrier($carrierId);
4947 if ($cType eq "default_sms_carrier_id") {
4948 $hold->sms_carrier($newval);
4951 if ($cType =~ /notify/){
4952 # this is a notification pref change
4953 if ($cType =~ /email/) { $hold->email_notify($newval); }
4954 if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
4955 if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
4956 # the other case, where x_notify is changed to true,
4957 # is covered by an actual value being assigned
4960 $e->update_action_hold_request($hold) or return $e->die_event;
4965 $e->commit; #unless $U->event_code($res);
4971 __PACKAGE__->register_method(
4972 method => "retrieve_holds_by_usr_with_notify",
4973 api_name => "open-ils.circ.holds.retrieve.by_usr.with_notify",
4975 desc => "Retrieve the hold, for the specified user using the notify value. $ses_is_req_note",
4977 { desc => 'Authentication token', type => 'string' },
4978 { desc => 'User ID', type => 'number' },
4981 desc => 'Lists of holds with notification values, event on error',
4986 sub retrieve_holds_by_usr_with_notify {
4988 my($self, $conn, $auth, $usr_id) = @_;
4990 my $e = new_editor(authtoken=>$auth);
4991 $e->checkauth or return $e->event;
4993 if ($e->requestor->id != $usr_id){
4994 $e->allowed('VIEW_HOLD') or return $e->event;
4998 "select" => { "ahr" => ["id", "phone_notify", "email_notify", "sms_carrier", "sms_notify"]},
5002 "capture_time" => undef,
5003 "cancel_time" => undef,
5004 "fulfillment_time" => undef,
5008 my $holds = $e->json_query($q);
5012 __PACKAGE__->register_method(
5013 method => "batch_update_holds_by_value",
5014 api_name => "open-ils.circ.holds.batch_update_holds_by_notify",
5016 desc => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
5018 { desc => 'Authentication token', type => 'string' },
5019 { desc => 'User ID', type => 'number' },
5020 { desc => 'Hold IDs', type => 'array' },
5021 { desc => 'old notify value', type => 'string' },
5022 { desc => 'new notify value', type => 'string' },
5023 { desc => 'notify_type', type => 'string' }
5026 desc => 'Hold objects with transits attached, event on error',
5031 sub batch_update_holds_by_value {
5032 my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType) = @_;
5034 my $e = new_editor(authtoken=>$auth, xact=>1);
5035 $e->checkauth or return $e->event;
5036 if ($e->requestor->id != $usr_id){
5037 $e->allowed('UPDATE_HOLD') or return $e->event;
5041 for my $id (@$hold_ids) {
5043 my $hold = $e->retrieve_action_hold_request(int($id));
5045 if ($cType eq "day_phone" or $cType eq "evening_phone" or
5046 $cType eq "other_phone" or $cType eq "default_phone") {
5047 # change phone number value on hold
5048 $hold->phone_notify($newval);
5050 if ($cType eq "default_sms") {
5051 # change SMS number value on hold
5052 $hold->sms_notify($newval);
5055 if ($cType eq "default_sms_carrier_id") {
5056 $hold->sms_carrier(int($newval));
5059 if ($cType =~ /notify/){
5060 # this is a notification pref change
5061 if ($cType =~ /email/) { $hold->email_notify($newval); }
5062 if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
5063 if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
5064 # the other case, where x_notify is changed to true,
5065 # is covered by an actual value being assigned
5068 $e->update_action_hold_request($hold) or return $e->die_event;
5073 $e->commit; #unless $U->event_code($res);
5077 __PACKAGE__->register_method(
5078 method => "hold_metadata",
5079 api_name => "open-ils.circ.hold.get_metadata",
5084 Returns a stream of objects containing whatever bib,
5085 volume, etc. data is available to the specific hold
5089 {desc => 'Hold Type', type => 'string'},
5090 {desc => 'Hold Target(s)', type => 'number or array'},
5091 {desc => 'Context org unit (optional)', type => 'number'}
5095 Stream of hold metadata objects.
5104 my ($self, $client, $hold_type, $hold_targets, $org_id) = @_;
5106 $hold_targets = [$hold_targets] unless ref $hold_targets;
5108 my $e = new_editor();
5109 for my $target (@$hold_targets) {
5111 # create a dummy hold for find_hold_mvr
5112 my $hold = Fieldmapper::action::hold_request->new;
5113 $hold->hold_type($hold_type);
5114 $hold->target($target);
5116 my (undef, $volume, $copy, $issuance, $part, $bre, $metarecord) =
5117 find_hold_mvr($e, $hold, {suppress_mvr => 1});
5119 $bre->clear_marc; # avoid bulk
5125 issuance => $issuance,
5129 metarecord => $metarecord,
5130 metarecord_filters => {}
5133 # If this is a bib hold or metarecord hold, also return the
5134 # available set of MR filters (AKA "Holdable Formats") for the
5135 # hold. For bib holds these may be used to upgrade the hold
5136 # from a bib to metarecord hold.
5137 if ($hold_type eq 'T') {
5138 my $map = $e->search_metabib_metarecord_source_map(
5139 {source => $meta->{bibrecord}->id})->[0];
5142 $meta->{metarecord} =
5143 $e->retrieve_metabib_metarecord($map->metarecord);
5146 # Also fetch the available parts for bib-level holds.
5147 $meta->{parts} = $e->search_biblio_monograph_part(
5149 {record => $bre->id, deleted => 'f'},
5150 {order_by => {bmp => 'label_sortkey'}}
5155 if ($meta->{metarecord}) {
5158 $self->method_lookup('open-ils.circ.mmr.holds.filters')
5159 ->run($meta->{metarecord}->id, $org_id);
5162 $meta->{metarecord_filters} = $filters->{metarecord};
5166 $client->respond($meta);