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) {
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;
1323 if($U->is_true($hold->frozen)) {
1324 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1325 $hold->clear_current_copy;
1326 $hold->clear_prev_check_time;
1327 # Clear expire_time to prevent frozen holds from expiring.
1328 $logger->info("clearing expire_time for frozen hold ".$hold->id);
1329 $hold->clear_expire_time;
1332 # If the hold_expire_time is in the past && is not equal to the
1333 # original expire_time, then reset the expire time to be in the
1335 if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1 && $U->datecmp($hold->expire_time, $orig_hold->expire_time) != 0) {
1336 $hold->expire_time(calculate_expire_time($hold->request_lib));
1339 # If the hold is reactivated, reset the expire_time.
1340 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1341 $logger->info("Reset expire_time on activated hold ".$hold->id);
1342 $hold->expire_time(calculate_expire_time($hold->request_lib));
1345 $e->update_action_hold_request($hold) or return $e->die_event;
1348 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1349 $logger->info("Running targeter on activated hold ".$hold->id);
1350 $U->simplereq('open-ils.hold-targeter',
1351 'open-ils.hold-targeter.target', {hold => $hold->id});
1354 # a change to mint-condition changes the set of potential copies, so retarget the hold;
1355 if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
1356 _reset_hold($self, $e->requestor, $hold)
1357 } elsif($need_retarget && !defined $hold->capture_time()) { # If needed, retarget the hold due to changes
1358 $U->simplereq('open-ils.hold-targeter',
1359 'open-ils.hold-targeter.target', {hold => $hold->id});
1365 # this does not update the hold in the DB. It only
1366 # sets the shelf_expire_time field on the hold object.
1367 # start_time is optional and defaults to 'now'
1368 sub set_hold_shelf_expire_time {
1369 my ($class, $hold, $editor, $start_time) = @_;
1371 my $shelf_expire = $U->ou_ancestor_setting_value(
1373 'circ.holds.default_shelf_expire_interval',
1377 return undef unless $shelf_expire;
1379 $start_time = ($start_time) ?
1380 DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time)) :
1381 DateTime->now(time_zone => 'local'); # without time_zone we get UTC ... yuck!
1383 my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($shelf_expire);
1384 my $expire_time = $start_time->add(seconds => $seconds);
1386 # if the shelf expire time overlaps with a pickup lib's
1387 # closed date, push it out to the first open date
1388 my $dateinfo = $U->storagereq(
1389 'open-ils.storage.actor.org_unit.closed_date.overlap',
1390 $hold->pickup_lib, $expire_time->strftime('%FT%T%z'));
1393 my $dt_parser = DateTime::Format::ISO8601->new;
1394 $expire_time = $dt_parser->parse_datetime(clean_ISO8601($dateinfo->{end}));
1396 # TODO: enable/disable time bump via setting?
1397 $expire_time->set(hour => '23', minute => '59', second => '59');
1399 $logger->info("circulator: shelf_expire_time overlaps".
1400 " with closed date, pushing expire time to $expire_time");
1403 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
1409 my($e, $orig_hold, $hold, $copy) = @_;
1410 my $src = $orig_hold->pickup_lib;
1411 my $dest = $hold->pickup_lib;
1413 $logger->info("putting hold into transit on pickup_lib update");
1415 my $transit = Fieldmapper::action::hold_transit_copy->new;
1416 $transit->hold($hold->id);
1417 $transit->source($src);
1418 $transit->dest($dest);
1419 $transit->target_copy($copy->id);
1420 $transit->source_send_time('now');
1421 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1423 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1424 $copy->editor($e->requestor->id);
1425 $copy->edit_date('now');
1427 $e->create_action_hold_transit_copy($transit) or return $e->die_event;
1428 $e->update_asset_copy($copy) or return $e->die_event;
1432 # if the hold is frozen, this method ensures that the hold is not "targeted",
1433 # that is, it clears the current_copy and prev_check_time to essentiallly
1434 # reset the hold. If it is being activated, it runs the targeter in the background
1435 sub update_hold_if_frozen {
1436 my($self, $e, $hold, $orig_hold) = @_;
1437 return if $hold->capture_time;
1439 if($U->is_true($hold->frozen)) {
1440 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1441 $hold->clear_current_copy;
1442 $hold->clear_prev_check_time;
1445 if($U->is_true($orig_hold->frozen)) {
1446 $logger->info("Running targeter on activated hold ".$hold->id);
1447 $U->simplereq('open-ils.hold-targeter',
1448 'open-ils.hold-targeter.target', {hold => $hold->id});
1453 __PACKAGE__->register_method(
1454 method => "hold_note_CUD",
1455 api_name => "open-ils.circ.hold_request.note.cud",
1457 desc => 'Create, update or delete a hold request note. If the operator (from Auth. token) '
1458 . 'is not the owner of the hold, the UPDATE_HOLD permission is required',
1460 { desc => 'Authentication token', type => 'string' },
1461 { desc => 'Hold note object', type => 'object' }
1464 desc => 'Returns the note ID, event on error'
1470 my($self, $conn, $auth, $note) = @_;
1472 my $e = new_editor(authtoken => $auth, xact => 1);
1473 return $e->die_event unless $e->checkauth;
1475 my $hold = $e->retrieve_action_hold_request($note->hold)
1476 or return $e->die_event;
1478 if($hold->usr ne $e->requestor->id) {
1479 my $usr = $e->retrieve_actor_user($hold->usr);
1480 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1481 $note->staff('t') if $note->isnew;
1485 $e->create_action_hold_request_note($note) or return $e->die_event;
1486 } elsif($note->ischanged) {
1487 $e->update_action_hold_request_note($note) or return $e->die_event;
1488 } elsif($note->isdeleted) {
1489 $e->delete_action_hold_request_note($note) or return $e->die_event;
1497 __PACKAGE__->register_method(
1498 method => "retrieve_hold_status",
1499 api_name => "open-ils.circ.hold.status.retrieve",
1501 desc => 'Calculates the current status of the hold. The requestor must have ' .
1502 'VIEW_HOLD permissions if the hold is for a user other than the requestor' ,
1504 { desc => 'Hold ID', type => 'number' }
1507 # type => 'number', # event sometimes
1508 desc => <<'END_OF_DESC'
1509 Returns event on error or:
1510 -1 on error (for now),
1511 1 for 'waiting for copy to become available',
1512 2 for 'waiting for copy capture',
1515 5 for 'hold-shelf-delay'
1518 8 for 'captured, on wrong hold shelf'
1525 sub retrieve_hold_status {
1526 my($self, $client, $auth, $hold_id) = @_;
1528 my $e = new_editor(authtoken => $auth);
1529 return $e->event unless $e->checkauth;
1530 my $hold = $e->retrieve_action_hold_request($hold_id)
1531 or return $e->event;
1533 if( $e->requestor->id != $hold->usr ) {
1534 return $e->event unless $e->allowed('VIEW_HOLD');
1537 return _hold_status($e, $hold);
1543 if ($hold->cancel_time) {
1546 if ($U->is_true($hold->frozen) && !$hold->capture_time) {
1549 if ($hold->current_shelf_lib and $hold->current_shelf_lib ne $hold->pickup_lib) {
1552 if ($hold->fulfillment_time) {
1555 return 1 unless $hold->current_copy;
1556 return 2 unless $hold->capture_time;
1558 my $copy = $hold->current_copy;
1559 unless( ref $copy ) {
1560 $copy = $e->retrieve_asset_copy($hold->current_copy)
1561 or return $e->event;
1564 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
1566 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
1568 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
1569 return 4 unless $hs_wait_interval;
1571 # if a hold_shelf_status_delay interval is defined and start_time plus
1572 # the interval is greater than now, consider the hold to be in the virtual
1573 # "on its way to the holds shelf" status. Return 5.
1575 my $transit = $e->search_action_hold_transit_copy({
1577 target_copy => $copy->id,
1578 cancel_time => undef,
1579 dest_recv_time => {'!=' => undef},
1581 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
1582 $start_time = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time));
1583 my $end_time = $start_time->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($hs_wait_interval));
1585 return 5 if $end_time > DateTime->now;
1594 __PACKAGE__->register_method(
1595 method => "retrieve_hold_queue_stats",
1596 api_name => "open-ils.circ.hold.queue_stats.retrieve",
1598 desc => 'Returns summary data about the state of a hold',
1600 { desc => 'Authentication token', type => 'string'},
1601 { desc => 'Hold ID', type => 'number'},
1604 desc => q/Summary object with keys:
1605 total_holds : total holds in queue
1606 queue_position : current queue position
1607 potential_copies : number of potential copies for this hold
1608 estimated_wait : estimated wait time in days
1609 status : hold status
1610 -1 => error or unexpected state,
1611 1 => 'waiting for copy to become available',
1612 2 => 'waiting for copy capture',
1615 5 => 'hold-shelf-delay'
1622 sub retrieve_hold_queue_stats {
1623 my($self, $conn, $auth, $hold_id) = @_;
1624 my $e = new_editor(authtoken => $auth);
1625 return $e->event unless $e->checkauth;
1626 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
1627 if($e->requestor->id != $hold->usr) {
1628 return $e->event unless $e->allowed('VIEW_HOLD');
1630 return retrieve_hold_queue_status_impl($e, $hold);
1633 sub retrieve_hold_queue_status_impl {
1637 # The holds queue is defined as the distinct set of holds that share at
1638 # least one potential copy with the context hold, plus any holds that
1639 # share the same hold type and target. The latter part exists to
1640 # accomodate holds that currently have no potential copies
1641 my $q_holds = $e->json_query({
1643 # fetch cut_in_line and request_time since they're in the order_by
1644 # and we're asking for distinct values
1645 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1649 select => { ahcm => ['hold'] },
1654 'field' => 'target_copy',
1655 'fkey' => 'target_copy'
1659 where => { '+ahcm2' => { hold => $hold->id } },
1666 "field" => "cut_in_line",
1667 "transform" => "coalesce",
1669 "direction" => "desc"
1671 { "class" => "ahr", "field" => "request_time" }
1676 if (!@$q_holds) { # none? maybe we don't have a map ...
1677 $q_holds = $e->json_query({
1678 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1683 "field" => "cut_in_line",
1684 "transform" => "coalesce",
1686 "direction" => "desc"
1688 { "class" => "ahr", "field" => "request_time" }
1691 hold_type => $hold->hold_type,
1692 target => $hold->target,
1693 capture_time => undef,
1694 cancel_time => undef,
1696 {expire_time => undef },
1697 {expire_time => {'>' => 'now'}}
1705 for my $h (@$q_holds) {
1706 last if $h->{id} == $hold->id;
1710 my $hold_data = $e->json_query({
1712 acp => [ {column => 'id', transform => 'count', aggregate => 1, alias => 'count'} ],
1713 ccm => [ {column =>'avg_wait_time'} ]
1719 ccm => {type => 'left'}
1724 where => {'+ahcm' => {hold => $hold->id} }
1727 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1729 my $default_wait = $U->ou_ancestor_setting_value(
1730 $user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL, $e);
1731 my $min_wait = $U->ou_ancestor_setting_value(
1732 $user_org, 'circ.holds.min_estimated_wait_interval', $e);
1733 $min_wait = OpenILS::Utils::DateTime->interval_to_seconds($min_wait || '0 seconds');
1734 $default_wait ||= '0 seconds';
1736 # Estimated wait time is the average wait time across the set
1737 # of potential copies, divided by the number of potential copies
1738 # times the queue position.
1740 my $combined_secs = 0;
1741 my $num_potentials = 0;
1743 for my $wait_data (@$hold_data) {
1744 my $count += $wait_data->{count};
1745 $combined_secs += $count *
1746 OpenILS::Utils::DateTime->interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
1747 $num_potentials += $count;
1750 my $estimated_wait = -1;
1752 if($num_potentials) {
1753 my $avg_wait = $combined_secs / $num_potentials;
1754 $estimated_wait = $qpos * ($avg_wait / $num_potentials);
1755 $estimated_wait = $min_wait if $estimated_wait < $min_wait and $estimated_wait != -1;
1759 total_holds => scalar(@$q_holds),
1760 queue_position => $qpos,
1761 potential_copies => $num_potentials,
1762 status => _hold_status( $e, $hold ),
1763 estimated_wait => int($estimated_wait)
1768 sub fetch_open_hold_by_current_copy {
1771 my $hold = $apputils->simplereq(
1773 'open-ils.cstore.direct.action.hold_request.search.atomic',
1774 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1775 return $hold->[0] if ref($hold);
1779 sub fetch_related_holds {
1782 return $apputils->simplereq(
1784 'open-ils.cstore.direct.action.hold_request.search.atomic',
1785 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1789 __PACKAGE__->register_method(
1790 method => "hold_pull_list",
1791 api_name => "open-ils.circ.hold_pull_list.retrieve",
1793 desc => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1794 'The location is determined by the login session.',
1796 { desc => 'Limit (optional)', type => 'number'},
1797 { desc => 'Offset (optional)', type => 'number'},
1800 desc => 'reference to a list of holds, or event on failure',
1805 __PACKAGE__->register_method(
1806 method => "hold_pull_list",
1807 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
1809 desc => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
1810 'The location is determined by the login session.',
1812 { desc => 'Limit (optional)', type => 'number'},
1813 { desc => 'Offset (optional)', type => 'number'},
1816 desc => 'reference to a list of holds, or event on failure',
1821 __PACKAGE__->register_method(
1822 method => "hold_pull_list",
1823 api_name => "open-ils.circ.hold_pull_list.retrieve.count",
1825 desc => 'Returns a count of holds that need to be "pulled" by a given location. ' .
1826 'The location is determined by the login session.',
1828 { desc => 'Limit (optional)', type => 'number'},
1829 { desc => 'Offset (optional)', type => 'number'},
1832 desc => 'Holds count (integer), or event on failure',
1838 __PACKAGE__->register_method(
1839 method => "hold_pull_list",
1841 # TODO: tag with api_level 2 once fully supported
1842 api_name => "open-ils.circ.hold_pull_list.fleshed.stream",
1844 desc => q/Returns a stream of fleshed holds that need to be
1845 "pulled" by a given location. The location is
1846 determined by the login session.
1847 This API calls always run in authoritative mode./,
1849 { desc => 'Limit (optional)', type => 'number'},
1850 { desc => 'Offset (optional)', type => 'number'},
1853 desc => 'Stream of holds holds, or event on failure',
1858 sub hold_pull_list {
1859 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1860 my( $reqr, $evt ) = $U->checkses($authtoken);
1861 return $evt if $evt;
1863 my $org = $reqr->ws_ou || $reqr->home_ou;
1864 # the perm locaiton shouldn't really matter here since holds
1865 # will exist all over and VIEW_HOLDS should be universal
1866 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1867 return $evt if $evt;
1869 if($self->api_name =~ /count/) {
1871 my $count = $U->storagereq(
1872 'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1873 $org, $limit, $offset );
1875 $logger->info("Grabbing pull list for org unit $org with $count items");
1878 } elsif( $self->api_name =~ /id_list/ ) {
1880 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1881 $org, $limit, $offset );
1883 } elsif ($self->api_name =~ /fleshed/) {
1885 my $ids = $U->storagereq(
1886 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1887 $org, $limit, $offset );
1889 my $e = new_editor(xact => 1, requestor => $reqr);
1890 $conn->respond(uber_hold_impl($e, $_, {flesh_acpl => 1})) for @$ids;
1892 $conn->respond_complete;
1897 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1898 $org, $limit, $offset );
1902 __PACKAGE__->register_method(
1903 method => "print_hold_pull_list",
1904 api_name => "open-ils.circ.hold_pull_list.print",
1906 desc => 'Returns an HTML-formatted holds pull list',
1908 { desc => 'Authtoken', type => 'string'},
1909 { desc => 'Org unit ID. Optional, defaults to workstation org unit', type => 'number'},
1912 desc => 'HTML string',
1918 sub print_hold_pull_list {
1919 my($self, $client, $auth, $org_id) = @_;
1921 my $e = new_editor(authtoken=>$auth);
1922 return $e->event unless $e->checkauth;
1924 $org_id = (defined $org_id) ? $org_id : $e->requestor->ws_ou;
1925 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1927 my $hold_ids = $U->storagereq(
1928 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1931 return undef unless @$hold_ids;
1933 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1935 # Holds will /NOT/ be in order after this ...
1936 my $holds = $e->search_action_hold_request({id => $hold_ids}, {substream => 1});
1937 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1939 # ... so we must resort.
1940 my $hold_map = +{map { $_->id => $_ } @$holds};
1941 my $sorted_holds = [];
1942 push @$sorted_holds, $hold_map->{$_} foreach @$hold_ids;
1944 return $U->fire_object_event(
1945 undef, "ahr.format.pull_list", $sorted_holds,
1946 $org_id, undef, undef, $client
1951 __PACKAGE__->register_method(
1952 method => "print_hold_pull_list_stream",
1954 api_name => "open-ils.circ.hold_pull_list.print.stream",
1956 desc => 'Returns a stream of fleshed holds',
1958 { desc => 'Authtoken', type => 'string'},
1959 { 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)',
1964 desc => 'A stream of fleshed holds',
1970 sub print_hold_pull_list_stream {
1971 my($self, $client, $auth, $params) = @_;
1973 my $e = new_editor(authtoken=>$auth);
1974 return $e->die_event unless $e->checkauth;
1976 delete($$params{org_id}) unless (int($$params{org_id}));
1977 delete($$params{limit}) unless (int($$params{limit}));
1978 delete($$params{offset}) unless (int($$params{offset}));
1979 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
1980 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
1981 $$params{chunk_size} ||= 10;
1982 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
1984 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
1985 return $e->die_event unless $e->allowed('VIEW_HOLD', $$params{org_id });
1988 if ($$params{sort} && @{ $$params{sort} }) {
1989 for my $s (@{ $$params{sort} }) {
1990 if ($s eq 'acplo.position') {
1992 "class" => "acplo", "field" => "position",
1993 "transform" => "coalesce", "params" => [999]
1995 } elsif ($s eq 'prefix') {
1996 push @$sort, {"class" => "acnp", "field" => "label_sortkey"};
1997 } elsif ($s eq 'call_number') {
1998 push @$sort, {"class" => "acn", "field" => "label_sortkey"};
1999 } elsif ($s eq 'suffix') {
2000 push @$sort, {"class" => "acns", "field" => "label_sortkey"};
2001 } elsif ($s eq 'request_time') {
2002 push @$sort, {"class" => "ahr", "field" => "request_time"};
2006 push @$sort, {"class" => "ahr", "field" => "request_time"};
2009 my $holds_ids = $e->json_query(
2011 "select" => {"ahr" => ["id"]},
2016 "fkey" => "current_copy",
2018 "circ_lib" => $$params{org_id}, "status" => [0,7]
2023 "fkey" => "call_number",
2037 "fkey" => "circ_lib",
2040 "location" => {"=" => {"+acp" => "location"}}
2049 "capture_time" => undef,
2050 "cancel_time" => undef,
2052 {"expire_time" => undef },
2053 {"expire_time" => {">" => "now"}}
2057 (@$sort ? (order_by => $sort) : ()),
2058 ($$params{limit} ? (limit => $$params{limit}) : ()),
2059 ($$params{offset} ? (offset => $$params{offset}) : ())
2060 }, {"substream" => 1}
2061 ) or return $e->die_event;
2063 $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
2066 for my $hid (@$holds_ids) {
2067 push @chunk, $e->retrieve_action_hold_request([
2071 "ahr" => ["usr", "current_copy"],
2073 "acp" => ["location", "call_number", "parts"],
2074 "acn" => ["record","prefix","suffix"]
2079 if (@chunk >= $$params{chunk_size}) {
2080 $client->respond( \@chunk );
2084 $client->respond_complete( \@chunk ) if (@chunk);
2091 __PACKAGE__->register_method(
2092 method => 'fetch_hold_notify',
2093 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
2096 Returns a list of hold notification objects based on hold id.
2097 @param authtoken The loggin session key
2098 @param holdid The id of the hold whose notifications we want to retrieve
2099 @return An array of hold notification objects, event on error.
2103 sub fetch_hold_notify {
2104 my( $self, $conn, $authtoken, $holdid ) = @_;
2105 my( $requestor, $evt ) = $U->checkses($authtoken);
2106 return $evt if $evt;
2107 my ($hold, $patron);
2108 ($hold, $evt) = $U->fetch_hold($holdid);
2109 return $evt if $evt;
2110 ($patron, $evt) = $U->fetch_user($hold->usr);
2111 return $evt if $evt;
2113 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
2114 return $evt if $evt;
2116 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
2117 return $U->cstorereq(
2118 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
2122 __PACKAGE__->register_method(
2123 method => 'create_hold_notify',
2124 api_name => 'open-ils.circ.hold_notification.create',
2126 Creates a new hold notification object
2127 @param authtoken The login session key
2128 @param notification The hold notification object to create
2129 @return ID of the new object on success, Event on error
2133 sub create_hold_notify {
2134 my( $self, $conn, $auth, $note ) = @_;
2135 my $e = new_editor(authtoken=>$auth, xact=>1);
2136 return $e->die_event unless $e->checkauth;
2138 my $hold = $e->retrieve_action_hold_request($note->hold)
2139 or return $e->die_event;
2140 my $patron = $e->retrieve_actor_user($hold->usr)
2141 or return $e->die_event;
2143 return $e->die_event unless
2144 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
2146 $note->notify_staff($e->requestor->id);
2147 $e->create_action_hold_notification($note) or return $e->die_event;
2152 __PACKAGE__->register_method(
2153 method => 'create_hold_note',
2154 api_name => 'open-ils.circ.hold_note.create',
2156 Creates a new hold request note object
2157 @param authtoken The login session key
2158 @param note The hold note object to create
2159 @return ID of the new object on success, Event on error
2163 sub create_hold_note {
2164 my( $self, $conn, $auth, $note ) = @_;
2165 my $e = new_editor(authtoken=>$auth, xact=>1);
2166 return $e->die_event unless $e->checkauth;
2168 my $hold = $e->retrieve_action_hold_request($note->hold)
2169 or return $e->die_event;
2170 my $patron = $e->retrieve_actor_user($hold->usr)
2171 or return $e->die_event;
2173 return $e->die_event unless
2174 $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
2176 $e->create_action_hold_request_note($note) or return $e->die_event;
2181 __PACKAGE__->register_method(
2182 method => 'reset_hold',
2183 api_name => 'open-ils.circ.hold.reset',
2185 Un-captures and un-targets a hold, essentially returning
2186 it to the state it was in directly after it was placed,
2187 then attempts to re-target the hold
2188 @param authtoken The login session key
2189 @param holdid The id of the hold
2195 my( $self, $conn, $auth, $holdid ) = @_;
2197 my ($hold, $evt) = $U->fetch_hold($holdid);
2198 return $evt if $evt;
2199 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
2200 return $evt if $evt;
2201 $evt = _reset_hold($self, $reqr, $hold);
2202 return $evt if $evt;
2207 __PACKAGE__->register_method(
2208 method => 'reset_hold_batch',
2209 api_name => 'open-ils.circ.hold.reset.batch'
2212 sub reset_hold_batch {
2213 my($self, $conn, $auth, $hold_ids) = @_;
2215 my $e = new_editor(authtoken => $auth);
2216 return $e->event unless $e->checkauth;
2218 for my $hold_id ($hold_ids) {
2220 my $hold = $e->retrieve_action_hold_request(
2221 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
2222 or return $e->event;
2224 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
2225 _reset_hold($self, $e->requestor, $hold);
2233 my ($self, $reqr, $hold) = @_;
2235 my $e = new_editor(xact =>1, requestor => $reqr);
2237 $logger->info("reseting hold ".$hold->id);
2239 my $hid = $hold->id;
2241 if( $hold->capture_time and $hold->current_copy ) {
2243 my $copy = $e->retrieve_asset_copy($hold->current_copy)
2244 or return $e->die_event;
2246 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2247 $logger->info("setting copy to status 'reshelving' on hold retarget");
2248 $copy->status(OILS_COPY_STATUS_RESHELVING);
2249 $copy->editor($e->requestor->id);
2250 $copy->edit_date('now');
2251 $e->update_asset_copy($copy) or return $e->die_event;
2253 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
2255 $logger->warn("! reseting hold [$hid] that is in transit");
2256 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
2259 my $trans = $e->retrieve_action_transit_copy($transid);
2261 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
2262 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
2263 $logger->info("Transit abort completed with result $evt");
2264 unless ("$evt" eq 1) {
2273 $hold->clear_capture_time;
2274 $hold->clear_current_copy;
2275 $hold->clear_shelf_time;
2276 $hold->clear_shelf_expire_time;
2277 $hold->clear_current_shelf_lib;
2279 $e->update_action_hold_request($hold) or return $e->die_event;
2282 $U->simplereq('open-ils.hold-targeter',
2283 'open-ils.hold-targeter.target', {hold => $hold->id});
2289 __PACKAGE__->register_method(
2290 method => 'fetch_open_title_holds',
2291 api_name => 'open-ils.circ.open_holds.retrieve',
2293 Returns a list ids of un-fulfilled holds for a given title id
2294 @param authtoken The login session key
2295 @param id the id of the item whose holds we want to retrieve
2296 @param type The hold type - M, T, I, V, C, F, R
2300 sub fetch_open_title_holds {
2301 my( $self, $conn, $auth, $id, $type, $org ) = @_;
2302 my $e = new_editor( authtoken => $auth );
2303 return $e->event unless $e->checkauth;
2306 $org ||= $e->requestor->ws_ou;
2308 # return $e->search_action_hold_request(
2309 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
2311 # XXX make me return IDs in the future ^--
2312 my $holds = $e->search_action_hold_request(
2315 cancel_time => undef,
2317 fulfillment_time => undef
2321 flesh_hold_transits($holds);
2326 sub flesh_hold_transits {
2328 for my $hold ( @$holds ) {
2330 $apputils->simplereq(
2332 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
2333 { hold => $hold->id, cancel_time => undef },
2334 { order_by => { ahtc => 'id desc' }, limit => 1 }
2340 sub flesh_hold_notices {
2341 my( $holds, $e ) = @_;
2342 $e ||= new_editor();
2344 for my $hold (@$holds) {
2345 my $notices = $e->search_action_hold_notification(
2347 { hold => $hold->id },
2348 { order_by => { anh => 'notify_time desc' } },
2353 $hold->notify_count(scalar(@$notices));
2355 my $n = $e->retrieve_action_hold_notification($$notices[0])
2356 or return $e->event;
2357 $hold->notify_time($n->notify_time);
2363 __PACKAGE__->register_method(
2364 method => 'fetch_captured_holds',
2365 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
2369 Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
2370 @param authtoken The login session key
2371 @param org The org id of the location in question
2372 @param match_copy A specific copy to limit to
2376 __PACKAGE__->register_method(
2377 method => 'fetch_captured_holds',
2378 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
2382 Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
2383 @param authtoken The login session key
2384 @param org The org id of the location in question
2385 @param match_copy A specific copy to limit to
2389 __PACKAGE__->register_method(
2390 method => 'fetch_captured_holds',
2391 api_name => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
2395 Returns list ids of shelf-expired un-fulfilled holds for a given title id
2396 @param authtoken The login session key
2397 @param org The org id of the location in question
2398 @param match_copy A specific copy to limit to
2402 __PACKAGE__->register_method(
2403 method => 'fetch_captured_holds',
2405 'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
2409 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2410 for a given shelf lib
2414 __PACKAGE__->register_method(
2415 method => 'fetch_captured_holds',
2417 'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
2421 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2422 for a given shelf lib
2427 sub fetch_captured_holds {
2428 my( $self, $conn, $auth, $org, $match_copy ) = @_;
2430 my $e = new_editor(authtoken => $auth);
2431 return $e->die_event unless $e->checkauth;
2432 return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
2434 $org ||= $e->requestor->ws_ou;
2436 my $current_copy = { '!=' => undef };
2437 $current_copy = { '=' => $match_copy } if $match_copy;
2440 select => { alhr => ['id'] },
2445 fkey => 'current_copy'
2450 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
2452 capture_time => { "!=" => undef },
2453 current_copy => $current_copy,
2454 fulfillment_time => undef,
2455 current_shelf_lib => $org
2459 if($self->api_name =~ /expired/) {
2460 $query->{'where'}->{'+alhr'}->{'-or'} = {
2461 shelf_expire_time => { '<' => 'today'},
2462 cancel_time => { '!=' => undef },
2465 my $hold_ids = $e->json_query( $query );
2467 if ($self->api_name =~ /wrong_shelf/) {
2468 # fetch holds whose current_shelf_lib is $org, but whose pickup
2469 # lib is some other org unit. Ignore already-retrieved holds.
2471 pickup_lib_changed_on_shelf_holds(
2472 $e, $org, [map {$_->{id}} @$hold_ids]);
2473 # match the layout of other items in $hold_ids
2474 push (@$hold_ids, {id => $_}) for @$wrong_shelf;
2478 for my $hold_id (@$hold_ids) {
2479 if($self->api_name =~ /id_list/) {
2480 $conn->respond($hold_id->{id});
2484 $e->retrieve_action_hold_request([
2488 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
2489 order_by => {anh => 'notify_time desc'}
2499 __PACKAGE__->register_method(
2500 method => "print_expired_holds_stream",
2501 api_name => "open-ils.circ.captured_holds.expired.print.stream",
2505 sub print_expired_holds_stream {
2506 my ($self, $client, $auth, $params) = @_;
2508 # No need to check specific permissions: we're going to call another method
2509 # that will do that.
2510 my $e = new_editor("authtoken" => $auth);
2511 return $e->die_event unless $e->checkauth;
2513 delete($$params{org_id}) unless (int($$params{org_id}));
2514 delete($$params{limit}) unless (int($$params{limit}));
2515 delete($$params{offset}) unless (int($$params{offset}));
2516 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2517 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2518 $$params{chunk_size} ||= 10;
2519 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2521 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2523 my @hold_ids = $self->method_lookup(
2524 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
2525 )->run($auth, $params->{"org_id"});
2530 } elsif (defined $U->event_code($hold_ids[0])) {
2532 return $hold_ids[0];
2535 $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
2538 my @hid_chunk = splice @hold_ids, 0, $params->{"chunk_size"};
2540 my $result_chunk = $e->json_query({
2542 "acp" => ["barcode"],
2544 first_given_name second_given_name family_name alias
2553 "field" => "id", "fkey" => "current_copy",
2556 "field" => "id", "fkey" => "call_number",
2559 "field" => "id", "fkey" => "record"
2563 "acpl" => {"field" => "id", "fkey" => "location"}
2566 "au" => {"field" => "id", "fkey" => "usr"}
2569 "where" => {"+ahr" => {"id" => \@hid_chunk}}
2570 }) or return $e->die_event;
2571 $client->respond($result_chunk);
2578 __PACKAGE__->register_method(
2579 method => "check_title_hold_batch",
2580 api_name => "open-ils.circ.title_hold.is_possible.batch",
2583 desc => '@see open-ils.circ.title_hold.is_possible.batch',
2585 { desc => 'Authentication token', type => 'string'},
2586 { desc => 'Array of Hash of named parameters', type => 'array'},
2589 desc => 'Array of response objects',
2595 sub check_title_hold_batch {
2596 my($self, $client, $authtoken, $param_list, $oargs) = @_;
2597 foreach (@$param_list) {
2598 my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
2599 $client->respond($res);
2605 __PACKAGE__->register_method(
2606 method => "check_title_hold",
2607 api_name => "open-ils.circ.title_hold.is_possible",
2609 desc => 'Determines if a hold were to be placed by a given user, ' .
2610 'whether or not said hold would have any potential copies to fulfill it.' .
2611 'The named paramaters of the second argument include: ' .
2612 'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
2613 'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
2615 { desc => 'Authentication token', type => 'string'},
2616 { desc => 'Hash of named parameters', type => 'object'},
2619 desc => 'List of new message IDs (empty if none)',
2625 =head3 check_title_hold (token, hash)
2627 The named fields in the hash are:
2629 patronid - ID of the hold recipient (required)
2630 depth - hold range depth (default 0)
2631 pickup_lib - destination for hold, fallback value for selection_ou
2632 selection_ou - ID of org_unit establishing hard and soft hold boundary settings
2633 issuanceid - ID of the issuance to be held, required for Issuance level hold
2634 partid - ID of the monograph part to be held, required for monograph part level hold
2635 titleid - ID (BRN) of the title to be held, required for Title level hold
2636 volume_id - required for Volume level hold
2637 copy_id - required for Copy level hold
2638 mrid - required for Meta-record level hold
2639 hold_type - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record (default "T")
2641 All key/value pairs are passed on to do_possibility_checks.
2645 # FIXME: better params checking. what other params are required, if any?
2646 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
2647 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
2648 # used in conditionals, where it may be undefined, causing a warning.
2649 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
2651 sub check_title_hold {
2652 my( $self, $client, $authtoken, $params ) = @_;
2653 my $e = new_editor(authtoken=>$authtoken);
2654 return $e->event unless $e->checkauth;
2656 my %params = %$params;
2657 my $depth = $params{depth} || 0;
2658 $params{depth} = $depth; #define $params{depth} if unset, since it gets used later
2659 my $selection_ou = $params{selection_ou} || $params{pickup_lib};
2660 my $oargs = $params{oargs} || {};
2662 if($oargs->{events}) {
2663 @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
2667 my $patron = $e->retrieve_actor_user($params{patronid})
2668 or return $e->event;
2670 if( $e->requestor->id ne $patron->id ) {
2671 return $e->event unless
2672 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
2675 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
2677 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
2678 or return $e->event;
2680 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
2681 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
2684 my $return_depth = $hard_boundary; # default depth to return on success
2685 if(defined $soft_boundary and $depth < $soft_boundary) {
2686 # work up the tree and as soon as we find a potential copy, use that depth
2687 # also, make sure we don't go past the hard boundary if it exists
2689 # our min boundary is the greater of user-specified boundary or hard boundary
2690 my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
2691 $hard_boundary : $depth;
2693 my $depth = $soft_boundary;
2694 while($depth >= $min_depth) {
2695 $logger->info("performing hold possibility check with soft boundary $depth");
2696 @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
2698 $return_depth = $depth;
2703 } elsif(defined $hard_boundary and $depth < $hard_boundary) {
2704 # there is no soft boundary, enforce the hard boundary if it exists
2705 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
2706 @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
2708 # no boundaries defined, fall back to user specifed boundary or no boundary
2709 $logger->info("performing hold possibility check with no boundary");
2710 @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
2713 my $place_unfillable = 0;
2714 $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
2719 "depth" => $return_depth,
2720 "local_avail" => $status[1]
2722 } elsif ($status[2]) {
2723 my $n = scalar @{$status[2]};
2724 return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2726 return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2732 sub do_possibility_checks {
2733 my($e, $patron, $request_lib, $depth, %params) = @_;
2735 my $issuanceid = $params{issuanceid} || "";
2736 my $partid = $params{partid} || "";
2737 my $titleid = $params{titleid} || "";
2738 my $volid = $params{volume_id};
2739 my $copyid = $params{copy_id};
2740 my $mrid = $params{mrid} || "";
2741 my $pickup_lib = $params{pickup_lib};
2742 my $hold_type = $params{hold_type} || 'T';
2743 my $selection_ou = $params{selection_ou} || $pickup_lib;
2744 my $holdable_formats = $params{holdable_formats};
2745 my $oargs = $params{oargs} || {};
2752 if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
2754 return $e->event unless $copy = $e->retrieve_asset_copy($copyid);
2755 return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
2756 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2758 return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
2759 return verify_copy_for_hold(
2760 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
2763 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2765 return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
2766 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2768 return _check_volume_hold_is_possible(
2769 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2772 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
2774 return _check_title_hold_is_possible(
2775 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
2778 } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
2780 return _check_issuance_hold_is_possible(
2781 $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2784 } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
2786 return _check_monopart_hold_is_possible(
2787 $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2790 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2792 # pasing undef as the depth to filtered_records causes the depth
2793 # of the selection_ou to be used, which is not what we want here.
2796 my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
2798 for my $rec (@$recs) {
2799 @status = _check_title_hold_is_possible(
2800 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
2806 # else { Unrecognized hold_type ! } # FIXME: return error? or 0?
2809 sub MR_filter_records {
2816 my $opac_visible = shift;
2818 my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
2819 return $U->storagereq(
2820 'open-ils.storage.metarecord.filtered_records.atomic',
2821 $m, $f, $org_at_depth, $opac_visible
2824 __PACKAGE__->register_method(
2825 method => 'MR_filter_records',
2826 api_name => 'open-ils.circ.holds.metarecord.filtered_records',
2831 sub create_ranged_org_filter {
2832 my($e, $selection_ou, $depth) = @_;
2834 # find the orgs from which this hold may be fulfilled,
2835 # based on the selection_ou and depth
2837 my $top_org = $e->search_actor_org_unit([
2838 {parent_ou => undef},
2839 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
2842 return () if $depth == $top_org->ou_type->depth;
2844 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
2845 %org_filter = (circ_lib => []);
2846 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
2848 $logger->info("hold org filter at depth $depth and selection_ou ".
2849 "$selection_ou created list of @{$org_filter{circ_lib}}");
2855 sub _check_title_hold_is_possible {
2856 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
2857 # $holdable_formats is now unused. We pre-filter the MR's records.
2859 my $e = new_editor();
2860 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2862 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2863 my $copies = $e->json_query(
2865 select => { acp => ['id', 'circ_lib'] },
2870 fkey => 'call_number',
2871 filter => { record => $titleid }
2875 filter => { holdable => 't', deleted => 'f' },
2878 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' },
2879 acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2883 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2884 '+acpm' => { target_copy => undef } # ignore part-linked copies
2889 $logger->info("title possible found ".scalar(@$copies)." potential copies");
2893 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2894 "payload" => {"fail_part" => "no_ultimate_items"}
2899 # -----------------------------------------------------------------------
2900 # sort the copies into buckets based on their circ_lib proximity to
2901 # the patron's home_ou.
2902 # -----------------------------------------------------------------------
2904 my $home_org = $patron->home_ou;
2905 my $req_org = $request_lib->id;
2907 $prox_cache{$home_org} =
2908 $e->search_actor_org_unit_proximity({from_org => $home_org})
2909 unless $prox_cache{$home_org};
2910 my $home_prox = $prox_cache{$home_org};
2911 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2914 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2915 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2917 my @keys = sort { $a <=> $b } keys %buckets;
2920 if( $home_org ne $req_org ) {
2921 # -----------------------------------------------------------------------
2922 # shove the copies close to the request_lib into the primary buckets
2923 # directly before the farthest away copies. That way, they are not
2924 # given priority, but they are checked before the farthest copies.
2925 # -----------------------------------------------------------------------
2926 $prox_cache{$req_org} =
2927 $e->search_actor_org_unit_proximity({from_org => $req_org})
2928 unless $prox_cache{$req_org};
2929 my $req_prox = $prox_cache{$req_org};
2932 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2933 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2935 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2936 my $new_key = $highest_key - 0.5; # right before the farthest prox
2937 my @keys2 = sort { $a <=> $b } keys %buckets2;
2938 for my $key (@keys2) {
2939 last if $key >= $highest_key;
2940 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2944 @keys = sort { $a <=> $b } keys %buckets;
2949 my $age_protect_only = 0;
2950 OUTER: for my $key (@keys) {
2951 my @cps = @{$buckets{$key}};
2953 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2955 for my $copyid (@cps) {
2957 next if $seen{$copyid};
2958 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2959 my $copy = $e->retrieve_asset_copy($copyid);
2960 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2962 unless($title) { # grab the title if we don't already have it
2963 my $vol = $e->retrieve_asset_call_number(
2964 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2965 $title = $vol->record;
2968 @status = verify_copy_for_hold(
2969 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2971 $age_protect_only ||= $status[3];
2972 last OUTER if $status[0];
2976 $status[3] = $age_protect_only;
2980 sub _check_issuance_hold_is_possible {
2981 my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2983 my $e = new_editor();
2984 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2986 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2987 my $copies = $e->json_query(
2989 select => { acp => ['id', 'circ_lib'] },
2995 filter => { issuance => $issuanceid }
2999 filter => { holdable => 't', deleted => 'f' },
3002 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3006 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3012 $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
3016 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3017 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3022 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3023 "payload" => {"fail_part" => "no_ultimate_items"}
3031 # -----------------------------------------------------------------------
3032 # sort the copies into buckets based on their circ_lib proximity to
3033 # the patron's home_ou.
3034 # -----------------------------------------------------------------------
3036 my $home_org = $patron->home_ou;
3037 my $req_org = $request_lib->id;
3039 $prox_cache{$home_org} =
3040 $e->search_actor_org_unit_proximity({from_org => $home_org})
3041 unless $prox_cache{$home_org};
3042 my $home_prox = $prox_cache{$home_org};
3043 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3046 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3047 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3049 my @keys = sort { $a <=> $b } keys %buckets;
3052 if( $home_org ne $req_org ) {
3053 # -----------------------------------------------------------------------
3054 # shove the copies close to the request_lib into the primary buckets
3055 # directly before the farthest away copies. That way, they are not
3056 # given priority, but they are checked before the farthest copies.
3057 # -----------------------------------------------------------------------
3058 $prox_cache{$req_org} =
3059 $e->search_actor_org_unit_proximity({from_org => $req_org})
3060 unless $prox_cache{$req_org};
3061 my $req_prox = $prox_cache{$req_org};
3064 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3065 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3067 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
3068 my $new_key = $highest_key - 0.5; # right before the farthest prox
3069 my @keys2 = sort { $a <=> $b } keys %buckets2;
3070 for my $key (@keys2) {
3071 last if $key >= $highest_key;
3072 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3076 @keys = sort { $a <=> $b } keys %buckets;
3081 my $age_protect_only = 0;
3082 OUTER: for my $key (@keys) {
3083 my @cps = @{$buckets{$key}};
3085 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3087 for my $copyid (@cps) {
3089 next if $seen{$copyid};
3090 $seen{$copyid} = 1; # there could be dupes given the merged buckets
3091 my $copy = $e->retrieve_asset_copy($copyid);
3092 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3094 unless($title) { # grab the title if we don't already have it
3095 my $vol = $e->retrieve_asset_call_number(
3096 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3097 $title = $vol->record;
3100 @status = verify_copy_for_hold(
3101 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3103 $age_protect_only ||= $status[3];
3104 last OUTER if $status[0];
3109 if (!defined($empty_ok)) {
3110 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3111 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3114 return (1,0) if ($empty_ok);
3116 $status[3] = $age_protect_only;
3120 sub _check_monopart_hold_is_possible {
3121 my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3123 my $e = new_editor();
3124 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
3126 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
3127 my $copies = $e->json_query(
3129 select => { acp => ['id', 'circ_lib'] },
3133 field => 'target_copy',
3135 filter => { part => $partid }
3139 filter => { holdable => 't', deleted => 'f' },
3142 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3146 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3152 $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
3156 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3157 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3162 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3163 "payload" => {"fail_part" => "no_ultimate_items"}
3171 # -----------------------------------------------------------------------
3172 # sort the copies into buckets based on their circ_lib proximity to
3173 # the patron's home_ou.
3174 # -----------------------------------------------------------------------
3176 my $home_org = $patron->home_ou;
3177 my $req_org = $request_lib->id;
3179 $prox_cache{$home_org} =
3180 $e->search_actor_org_unit_proximity({from_org => $home_org})
3181 unless $prox_cache{$home_org};
3182 my $home_prox = $prox_cache{$home_org};
3183 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3186 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3187 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3189 my @keys = sort { $a <=> $b } keys %buckets;
3192 if( $home_org ne $req_org ) {
3193 # -----------------------------------------------------------------------
3194 # shove the copies close to the request_lib into the primary buckets
3195 # directly before the farthest away copies. That way, they are not
3196 # given priority, but they are checked before the farthest copies.
3197 # -----------------------------------------------------------------------
3198 $prox_cache{$req_org} =
3199 $e->search_actor_org_unit_proximity({from_org => $req_org})
3200 unless $prox_cache{$req_org};
3201 my $req_prox = $prox_cache{$req_org};
3204 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3205 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3207 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
3208 my $new_key = $highest_key - 0.5; # right before the farthest prox
3209 my @keys2 = sort { $a <=> $b } keys %buckets2;
3210 for my $key (@keys2) {
3211 last if $key >= $highest_key;
3212 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3216 @keys = sort { $a <=> $b } keys %buckets;
3221 my $age_protect_only = 0;
3222 OUTER: for my $key (@keys) {
3223 my @cps = @{$buckets{$key}};
3225 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3227 for my $copyid (@cps) {
3229 next if $seen{$copyid};
3230 $seen{$copyid} = 1; # there could be dupes given the merged buckets
3231 my $copy = $e->retrieve_asset_copy($copyid);
3232 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3234 unless($title) { # grab the title if we don't already have it
3235 my $vol = $e->retrieve_asset_call_number(
3236 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3237 $title = $vol->record;
3240 @status = verify_copy_for_hold(
3241 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3243 $age_protect_only ||= $status[3];
3244 last OUTER if $status[0];
3249 if (!defined($empty_ok)) {
3250 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3251 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3254 return (1,0) if ($empty_ok);
3256 $status[3] = $age_protect_only;
3261 sub _check_volume_hold_is_possible {
3262 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3263 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
3264 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
3265 $logger->info("checking possibility of volume hold for volume ".$vol->id);
3267 my $filter_copies = [];
3268 for my $copy (@$copies) {
3269 # ignore part-mapped copies for regular volume level holds
3270 push(@$filter_copies, $copy) unless
3271 new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
3273 $copies = $filter_copies;
3278 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3279 "payload" => {"fail_part" => "no_ultimate_items"}
3285 my $age_protect_only = 0;
3286 for my $copy ( @$copies ) {
3287 @status = verify_copy_for_hold(
3288 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
3289 $age_protect_only ||= $status[3];
3292 $status[3] = $age_protect_only;
3298 sub verify_copy_for_hold {
3299 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
3300 # $oargs should be undef unless we're overriding.
3301 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
3302 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3305 requestor => $requestor,
3308 title_descriptor => $title->fixed_fields,
3309 pickup_lib => $pickup_lib,
3310 request_lib => $request_lib,
3312 show_event_list => 1
3316 # Check for override permissions on events.
3317 if ($oargs && $permitted && scalar @$permitted) {
3318 # Remove the events from permitted that we can override.
3319 if ($oargs->{events}) {
3320 foreach my $evt (@{$oargs->{events}}) {
3321 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3324 # Now, we handle the override all case by checking remaining
3325 # events against override permissions.
3326 if (scalar @$permitted && $oargs->{all}) {
3327 # Pre-set events and failed members of oargs to empty
3328 # arrays, if they are not set, yet.
3329 $oargs->{events} = [] unless ($oargs->{events});
3330 $oargs->{failed} = [] unless ($oargs->{failed});
3331 # When we're done with these checks, we swap permitted
3332 # with a reference to @disallowed.
3333 my @disallowed = ();
3334 foreach my $evt (@{$permitted}) {
3335 # Check if we've already seen the event in this
3336 # session and it failed.
3337 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3338 push(@disallowed, $evt);
3340 # We have to check if the requestor has the
3341 # override permission.
3343 # AppUtils::check_user_perms returns the perm if
3344 # the user doesn't have it, undef if they do.
3345 if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3346 push(@disallowed, $evt);
3347 push(@{$oargs->{failed}}, $evt->{textcode});
3349 push(@{$oargs->{events}}, $evt->{textcode});
3353 $permitted = \@disallowed;
3357 my $age_protect_only = 0;
3358 if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3359 $age_protect_only = 1;
3363 (not scalar @$permitted), # true if permitted is an empty arrayref
3364 ( # XXX This test is of very dubious value; someone should figure
3365 # out what if anything is checking this value
3366 ($copy->circ_lib == $pickup_lib) and
3367 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3376 sub find_nearest_permitted_hold {
3379 my $editor = shift; # CStoreEditor object
3380 my $copy = shift; # copy to target
3381 my $user = shift; # staff
3382 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3384 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3386 my $bc = $copy->barcode;
3388 # find any existing holds that already target this copy
3389 my $old_holds = $editor->search_action_hold_request(
3390 { current_copy => $copy->id,
3391 cancel_time => undef,
3392 capture_time => undef
3396 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3398 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3399 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3401 my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3403 # the nearest_hold API call now needs this
3404 $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3405 unless ref $copy->call_number;
3407 # search for what should be the best holds for this copy to fulfill
3408 my $best_holds = $U->storagereq(
3409 "open-ils.storage.action.hold_request.nearest_hold.atomic",
3410 $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3412 # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3414 for my $holdid (@$old_holds) {
3415 next unless $holdid;
3416 push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3420 unless(@$best_holds) {
3421 $logger->info("circulator: no suitable holds found for copy $bc");
3422 return (undef, $evt);
3428 # for each potential hold, we have to run the permit script
3429 # to make sure the hold is actually permitted.
3432 for my $holdid (@$best_holds) {
3433 next unless $holdid;
3434 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3436 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3437 # Force and recall holds bypass all rules
3438 if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3442 my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3443 my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3445 $reqr_cache{$hold->requestor} = $reqr;
3446 $org_cache{$hold->request_lib} = $rlib;
3448 # see if this hold is permitted
3449 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3451 patron_id => $hold->usr,
3454 pickup_lib => $hold->pickup_lib,
3455 request_lib => $rlib,
3467 unless( $best_hold ) { # no "good" permitted holds were found
3469 $logger->info("circulator: no suitable holds found for copy $bc");
3470 return (undef, $evt);
3473 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3475 # indicate a permitted hold was found
3476 return $best_hold if $check_only;
3478 # we've found a permitted hold. we need to "grab" the copy
3479 # to prevent re-targeted holds (next part) from re-grabbing the copy
3480 $best_hold->current_copy($copy->id);
3481 $editor->update_action_hold_request($best_hold)
3482 or return (undef, $editor->event);
3487 # re-target any other holds that already target this copy
3488 for my $old_hold (@$old_holds) {
3489 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3490 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3491 $old_hold->id." after a better hold [".$best_hold->id."] was found");
3492 $old_hold->clear_current_copy;
3493 $old_hold->clear_prev_check_time;
3494 $editor->update_action_hold_request($old_hold)
3495 or return (undef, $editor->event);
3496 push(@retarget, $old_hold->id);
3499 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3507 __PACKAGE__->register_method(
3508 method => 'all_rec_holds',
3509 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3513 my( $self, $conn, $auth, $title_id, $args ) = @_;
3515 my $e = new_editor(authtoken=>$auth);
3516 $e->checkauth or return $e->event;
3517 $e->allowed('VIEW_HOLD') or return $e->event;
3520 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
3521 $args->{cancel_time} = undef;
3524 metarecord_holds => []
3526 , volume_holds => []
3528 , recall_holds => []
3531 , issuance_holds => []
3534 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3536 $resp->{metarecord_holds} = $e->search_action_hold_request(
3537 { hold_type => OILS_HOLD_TYPE_METARECORD,
3538 target => $mr_map->metarecord,
3544 $resp->{title_holds} = $e->search_action_hold_request(
3546 hold_type => OILS_HOLD_TYPE_TITLE,
3547 target => $title_id,
3551 my $parts = $e->search_biblio_monograph_part(
3557 $resp->{part_holds} = $e->search_action_hold_request(
3559 hold_type => OILS_HOLD_TYPE_MONOPART,
3565 my $subs = $e->search_serial_subscription(
3566 { record_entry => $title_id }, {idlist=>1});
3569 my $issuances = $e->search_serial_issuance(
3570 {subscription => $subs}, {idlist=>1}
3574 $resp->{issuance_holds} = $e->search_action_hold_request(
3576 hold_type => OILS_HOLD_TYPE_ISSUANCE,
3577 target => $issuances,
3584 my $vols = $e->search_asset_call_number(
3585 { record => $title_id, deleted => 'f' }, {idlist=>1});
3587 return $resp unless @$vols;
3589 $resp->{volume_holds} = $e->search_action_hold_request(
3591 hold_type => OILS_HOLD_TYPE_VOLUME,
3596 my $copies = $e->search_asset_copy(
3597 { call_number => $vols, deleted => 'f' }, {idlist=>1});
3599 return $resp unless @$copies;
3601 $resp->{copy_holds} = $e->search_action_hold_request(
3603 hold_type => OILS_HOLD_TYPE_COPY,
3608 $resp->{recall_holds} = $e->search_action_hold_request(
3610 hold_type => OILS_HOLD_TYPE_RECALL,
3615 $resp->{force_holds} = $e->search_action_hold_request(
3617 hold_type => OILS_HOLD_TYPE_FORCE,
3625 __PACKAGE__->register_method(
3626 method => 'stream_wide_holds',
3629 api_name => 'open-ils.circ.hold.wide_hash.stream'
3632 sub stream_wide_holds {
3633 my($self, $client, $auth, $restrictions, $order_by, $limit, $offset) = @_;
3635 my $e = new_editor(authtoken=>$auth);
3636 $e->checkauth or return $e->event;
3637 $e->allowed('VIEW_HOLD') or return $e->event;
3639 my $st = OpenSRF::AppSession->create('open-ils.storage');
3640 my $req = $st->request(
3641 'open-ils.storage.action.live_holds.wide_hash',
3642 $restrictions, $order_by, $limit, $offset
3645 my $count = $req->recv;
3650 if(UNIVERSAL::isa($count,"Error")) {
3651 throw $count ($count->stringify);
3654 $count = $count->content;
3656 # Force immediate send of count response
3657 my $mbc = $client->max_bundle_count;
3658 $client->max_bundle_count(1);
3659 $client->respond($count);
3660 $client->max_bundle_count($mbc);
3662 while (my $hold = $req->recv) {
3663 $client->respond($hold->content) if $hold->content;
3666 $client->respond_complete;
3672 __PACKAGE__->register_method(
3673 method => 'uber_hold',
3675 api_name => 'open-ils.circ.hold.details.retrieve'
3679 my($self, $client, $auth, $hold_id, $args) = @_;
3680 my $e = new_editor(authtoken=>$auth);
3681 $e->checkauth or return $e->event;
3682 return uber_hold_impl($e, $hold_id, $args);
3685 __PACKAGE__->register_method(
3686 method => 'batch_uber_hold',
3689 api_name => 'open-ils.circ.hold.details.batch.retrieve'
3692 sub batch_uber_hold {
3693 my($self, $client, $auth, $hold_ids, $args) = @_;
3694 my $e = new_editor(authtoken=>$auth);
3695 $e->checkauth or return $e->event;
3696 $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3700 sub uber_hold_impl {
3701 my($e, $hold_id, $args) = @_;
3704 my $flesh_fields = ['current_copy', 'usr', 'notes'];
3705 push (@$flesh_fields, 'requestor') if $args->{include_requestor};
3706 push (@$flesh_fields, 'cancel_cause') if $args->{include_cancel_cause};
3707 push (@$flesh_fields, 'sms_carrier') if $args->{include_sms_carrier};
3709 my $hold = $e->retrieve_action_hold_request([
3711 {flesh => 1, flesh_fields => {ahr => $flesh_fields}}
3712 ]) or return $e->event;
3714 if($hold->usr->id ne $e->requestor->id) {
3715 # caller is asking for someone else's hold
3716 $e->allowed('VIEW_HOLD') or return $e->event;
3717 $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3718 [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3721 # caller is asking for own hold, but may not have permission to view staff notes
3722 unless($e->allowed('VIEW_HOLD')) {
3723 $hold->notes( # filter out any staff notes (unless marked as public)
3724 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3728 my $user = $hold->usr;
3729 $hold->usr($user->id);
3732 my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args);
3734 flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3735 flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3737 my $details = retrieve_hold_queue_status_impl($e, $hold);
3738 $hold->usr($user) if $args->{include_usr}; # re-flesh
3743 ($copy ? (copy => $copy) : ()),
3744 ($volume ? (volume => $volume) : ()),
3745 ($issuance ? (issuance => $issuance) : ()),
3746 ($part ? (part => $part) : ()),
3747 ($args->{include_bre} ? (bre => $bre) : ()),
3748 ($args->{suppress_mvr} ? () : (mvr => $mvr)),
3752 $resp->{copy}->location(
3753 $e->retrieve_asset_copy_location($resp->{copy}->location))
3754 if $resp->{copy} and $args->{flesh_acpl};
3756 unless($args->{suppress_patron_details}) {
3757 my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3758 $resp->{patron_first} = $user->first_given_name,
3759 $resp->{patron_last} = $user->family_name,
3760 $resp->{patron_barcode} = $card->barcode,
3761 $resp->{patron_alias} = $user->alias,
3769 # -----------------------------------------------------
3770 # Returns the MVR object that represents what the
3772 # -----------------------------------------------------
3774 my( $e, $hold, $args ) = @_;
3782 my $no_mvr = $args->{suppress_mvr};
3784 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3785 $metarecord = $e->retrieve_metabib_metarecord($hold->target)
3786 or return $e->event;
3787 $tid = $metarecord->master_record;
3789 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3790 $tid = $hold->target;
3792 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3793 $volume = $e->retrieve_asset_call_number($hold->target)
3794 or return $e->event;
3795 $tid = $volume->record;
3797 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3798 $issuance = $e->retrieve_serial_issuance([
3800 {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3801 ]) or return $e->event;
3803 $tid = $issuance->subscription->record_entry;
3805 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3806 $part = $e->retrieve_biblio_monograph_part([
3808 ]) or return $e->event;
3810 $tid = $part->record;
3812 } 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 ) {
3813 $copy = $e->retrieve_asset_copy([
3815 {flesh => 1, flesh_fields => {acp => ['call_number']}}
3816 ]) or return $e->event;
3818 $volume = $copy->call_number;
3819 $tid = $volume->record;
3822 if(!$copy and ref $hold->current_copy ) {
3823 $copy = $hold->current_copy;
3824 $hold->current_copy($copy->id) unless $args->{include_current_copy};
3827 if(!$volume and $copy) {
3828 $volume = $e->retrieve_asset_call_number($copy->call_number);
3831 # TODO return metarcord mvr for M holds
3832 my $title = $e->retrieve_biblio_record_entry($tid);
3833 return ( ($no_mvr) ? undef : $U->record_to_mvr($title),
3834 $volume, $copy, $issuance, $part, $title, $metarecord);
3837 __PACKAGE__->register_method(
3838 method => 'clear_shelf_cache',
3839 api_name => 'open-ils.circ.hold.clear_shelf.get_cache',
3843 Returns the holds processed with the given cache key
3848 sub clear_shelf_cache {
3849 my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3850 my $e = new_editor(authtoken => $auth, xact => 1);
3851 return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3854 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3856 my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3859 $logger->info("no hold data found in cache"); # XXX TODO return event
3865 foreach (keys %$hold_data) {
3866 $maximum += scalar(@{ $hold_data->{$_} });
3868 $client->respond({"maximum" => $maximum, "progress" => 0});
3870 for my $action (sort keys %$hold_data) {
3871 while (@{$hold_data->{$action}}) {
3872 my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3874 my $result_chunk = $e->json_query({
3876 "acp" => ["barcode"],
3878 first_given_name second_given_name family_name alias
3881 "acnp" => [{column => "label", alias => "prefix"}],
3882 "acns" => [{column => "label", alias => "suffix"}],
3890 "field" => "id", "fkey" => "current_copy",
3893 "field" => "id", "fkey" => "call_number",
3896 "field" => "id", "fkey" => "record"
3899 "field" => "id", "fkey" => "prefix"
3902 "field" => "id", "fkey" => "suffix"
3906 "acpl" => {"field" => "id", "fkey" => "location"}
3909 "au" => {"field" => "id", "fkey" => "usr"}
3912 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3913 }, {"substream" => 1}) or return $e->die_event;
3917 +{"action" => $action, "hold_details" => $_}
3928 __PACKAGE__->register_method(
3929 method => 'clear_shelf_process',
3931 api_name => 'open-ils.circ.hold.clear_shelf.process',
3934 1. Find all holds that have expired on the holds shelf
3936 3. If a clear-shelf status is configured, put targeted copies into this status
3937 4. Divide copies into 3 groups: items to transit, items to reshelve, and items
3938 that are needed for holds. No subsequent action is taken on the holds
3939 or items after grouping.
3944 sub clear_shelf_process {
3945 my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
3947 my $e = new_editor(authtoken=>$auth);
3948 $e->checkauth or return $e->die_event;
3949 my $cache = OpenSRF::Utils::Cache->new('global');
3951 $org_id ||= $e->requestor->ws_ou;
3952 $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
3954 my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
3956 my @hold_ids = $self->method_lookup(
3957 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
3958 )->run($auth, $org_id, $match_copy);
3963 my @canceled_holds; # newly canceled holds
3964 $chunk_size ||= 25; # chunked status updates
3965 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3968 for my $hold_id (@hold_ids) {
3970 $logger->info("Clear shelf processing hold $hold_id");
3972 my $hold = $e->retrieve_action_hold_request([
3975 flesh_fields => {ahr => ['current_copy']}
3979 if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
3980 $hold->cancel_time('now');
3981 $hold->cancel_cause(2); # Hold Shelf expiration
3982 $e->update_action_hold_request($hold) or return $e->die_event;
3983 push(@canceled_holds, $hold_id);
3986 my $copy = $hold->current_copy;
3988 if($copy_status or $copy_status == 0) {
3989 # if a clear-shelf copy status is defined, update the copy
3990 $copy->status($copy_status);
3991 $copy->edit_date('now');
3992 $copy->editor($e->requestor->id);
3993 $e->update_asset_copy($copy) or return $e->die_event;
3996 push(@holds, $hold);
3997 $client->respond({maximum => scalar(@holds), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
4006 pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
4009 for my $hold (@holds) {
4011 my $copy = $hold->current_copy;
4012 my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
4014 if($alt_hold and !$match_copy) {
4016 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
4018 } elsif($copy->circ_lib != $e->requestor->ws_ou) {
4020 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
4024 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
4028 my $cache_key = md5_hex(time . $$ . rand());
4029 $logger->info("clear_shelf_cache: storing under $cache_key");
4030 $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours. configurable?
4032 # tell the client we're done
4033 $client->respond_complete({cache_key => $cache_key});
4036 # fire off the hold cancelation trigger and wait for response so don't flood the service
4038 # refetch the holds to pick up the caclulated cancel_time,
4039 # which may be needed by Action/Trigger
4041 my $updated_holds = [];
4042 $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
4045 $U->create_events_for_hook(
4046 'hold_request.cancel.expire_holds_shelf',
4047 $_, $org_id, undef, undef, 1) for @$updated_holds;
4050 # tell the client we're done
4051 $client->respond_complete;
4055 # returns IDs for holds that are on the holds shelf but
4056 # have had their pickup_libs change while on the shelf.
4057 sub pickup_lib_changed_on_shelf_holds {
4060 my $ignore_holds = shift;
4061 $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
4064 select => { alhr => ['id'] },
4069 fkey => 'current_copy'
4074 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
4076 capture_time => { "!=" => undef },
4077 fulfillment_time => undef,
4078 current_shelf_lib => $org_id,
4079 pickup_lib => {'!=' => {'+alhr' => 'current_shelf_lib'}}
4084 $query->{where}->{'+alhr'}->{id} =
4085 {'not in' => $ignore_holds} if @$ignore_holds;
4087 my $hold_ids = $e->json_query($query);
4088 return [ map { $_->{id} } @$hold_ids ];
4091 __PACKAGE__->register_method(
4092 method => 'usr_hold_summary',
4093 api_name => 'open-ils.circ.holds.user_summary',
4095 Returns a summary of holds statuses for a given user
4099 sub usr_hold_summary {
4100 my($self, $conn, $auth, $user_id) = @_;
4102 my $e = new_editor(authtoken=>$auth);
4103 $e->checkauth or return $e->event;
4104 $e->allowed('VIEW_HOLD') or return $e->event;
4106 my $holds = $e->search_action_hold_request(
4109 fulfillment_time => undef,
4110 cancel_time => undef,
4114 my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
4115 $summary{_hold_status($e, $_)} += 1 for @$holds;
4121 __PACKAGE__->register_method(
4122 method => 'hold_has_copy_at',
4123 api_name => 'open-ils.circ.hold.has_copy_at',
4126 'Returns the ID of the found copy and name of the shelving location if there is ' .
4127 'an available copy at the specified org unit. Returns empty hash otherwise. ' .
4128 'The anticipated use for this method is to determine whether an item is ' .
4129 'available at the library where the user is placing the hold (or, alternatively, '.
4130 'at the pickup library) to encourage bypassing the hold placement and just ' .
4131 'checking out the item.' ,
4133 { desc => 'Authentication Token', type => 'string' },
4134 { desc => 'Method Arguments. Options include: hold_type, hold_target, org_unit. '
4135 . 'hold_type is the hold type code (T, V, C, M, ...). '
4136 . 'hold_target is the identifier of the hold target object. '
4137 . 'org_unit is org unit ID.',
4142 desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
4148 sub hold_has_copy_at {
4149 my($self, $conn, $auth, $args) = @_;
4151 my $e = new_editor(authtoken=>$auth);
4152 $e->checkauth or return $e->event;
4154 my $hold_type = $$args{hold_type};
4155 my $hold_target = $$args{hold_target};
4156 my $org_unit = $$args{org_unit};
4159 select => {acp => ['id'], acpl => ['name']},
4164 filter => { holdable => 't', deleted => 'f' },
4167 ccs => {field => 'id', filter => { holdable => 't'}, fkey => 'status' }
4170 where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit, status => [0,7]}},
4174 if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
4176 $query->{where}->{'+acp'}->{id} = $hold_target;
4178 } elsif($hold_type eq 'V') {
4180 $query->{where}->{'+acp'}->{call_number} = $hold_target;
4182 } elsif($hold_type eq 'P') {
4184 $query->{from}->{acp}->{acpm} = {
4185 field => 'target_copy',
4187 filter => {part => $hold_target},
4190 } elsif($hold_type eq 'I') {
4192 $query->{from}->{acp}->{sitem} = {
4195 filter => {issuance => $hold_target},
4198 } elsif($hold_type eq 'T') {
4200 $query->{from}->{acp}->{acn} = {
4202 fkey => 'call_number',
4206 filter => {id => $hold_target},
4214 $query->{from}->{acp}->{acn} = {
4216 fkey => 'call_number',
4225 filter => {metarecord => $hold_target},
4233 my $res = $e->json_query($query)->[0] or return {};
4234 return {copy => $res->{id}, location => $res->{name}} if $res;
4238 # returns true if the user already has an item checked out
4239 # that could be used to fulfill the requested hold.
4240 sub hold_item_is_checked_out {
4241 my($e, $user_id, $hold_type, $hold_target) = @_;
4244 select => {acp => ['id']},
4245 from => {acp => {}},
4249 in => { # copies for circs the user has checked out
4250 select => {circ => ['target_copy']},
4254 checkin_time => undef,
4256 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
4257 {stop_fines => undef}
4267 if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
4269 $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
4271 } elsif($hold_type eq 'V') {
4273 $query->{where}->{'+acp'}->{call_number} = $hold_target;
4275 } elsif($hold_type eq 'P') {
4277 $query->{from}->{acp}->{acpm} = {
4278 field => 'target_copy',
4280 filter => {part => $hold_target},
4283 } elsif($hold_type eq 'I') {
4285 $query->{from}->{acp}->{sitem} = {
4288 filter => {issuance => $hold_target},
4291 } elsif($hold_type eq 'T') {
4293 $query->{from}->{acp}->{acn} = {
4295 fkey => 'call_number',
4299 filter => {id => $hold_target},
4307 $query->{from}->{acp}->{acn} = {
4309 fkey => 'call_number',
4318 filter => {metarecord => $hold_target},
4326 return $e->json_query($query)->[0];
4329 __PACKAGE__->register_method(
4330 method => 'change_hold_title',
4331 api_name => 'open-ils.circ.hold.change_title',
4334 Updates all title level holds targeting the specified bibs to point a new bib./,
4336 { desc => 'Authentication Token', type => 'string' },
4337 { desc => 'New Target Bib Id', type => 'number' },
4338 { desc => 'Old Target Bib Ids', type => 'array' },
4340 return => { desc => '1 on success' }
4344 __PACKAGE__->register_method(
4345 method => 'change_hold_title_for_specific_holds',
4346 api_name => 'open-ils.circ.hold.change_title.specific_holds',
4349 Updates specified holds to target new bib./,
4351 { desc => 'Authentication Token', type => 'string' },
4352 { desc => 'New Target Bib Id', type => 'number' },
4353 { desc => 'Holds Ids for holds to update', type => 'array' },
4355 return => { desc => '1 on success' }
4360 sub change_hold_title {
4361 my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4363 my $e = new_editor(authtoken=>$auth, xact=>1);
4364 return $e->die_event unless $e->checkauth;
4366 my $holds = $e->search_action_hold_request(
4369 capture_time => undef,
4370 cancel_time => undef,
4371 fulfillment_time => undef,
4377 flesh_fields => { ahr => ['usr'] }
4383 for my $hold (@$holds) {
4384 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4385 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4386 $hold->target( $new_bib_id );
4387 $e->update_action_hold_request($hold) or return $e->die_event;
4392 _reset_hold($self, $e->requestor, $_) for @$holds;
4397 sub change_hold_title_for_specific_holds {
4398 my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4400 my $e = new_editor(authtoken=>$auth, xact=>1);
4401 return $e->die_event unless $e->checkauth;
4403 my $holds = $e->search_action_hold_request(
4406 capture_time => undef,
4407 cancel_time => undef,
4408 fulfillment_time => undef,
4414 flesh_fields => { ahr => ['usr'] }
4420 for my $hold (@$holds) {
4421 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4422 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4423 $hold->target( $new_bib_id );
4424 $e->update_action_hold_request($hold) or return $e->die_event;
4429 _reset_hold($self, $e->requestor, $_) for @$holds;
4434 __PACKAGE__->register_method(
4435 method => 'rec_hold_count',
4436 api_name => 'open-ils.circ.bre.holds.count',
4438 desc => q/Returns the total number of holds that target the
4439 selected bib record or its associated copies and call_numbers/,
4441 { desc => 'Bib ID', type => 'number' },
4442 { desc => q/Optional arguments. Supported arguments include:
4443 "pickup_lib_descendant" -> limit holds to those whose pickup
4444 library is equal to or is a child of the provided org unit/,
4448 return => {desc => 'Hold count', type => 'number'}
4452 __PACKAGE__->register_method(
4453 method => 'rec_hold_count',
4454 api_name => 'open-ils.circ.mmr.holds.count',
4456 desc => q/Returns the total number of holds that target the
4457 selected metarecord or its associated copies, call_numbers, and bib records/,
4459 { desc => 'Metarecord ID', type => 'number' },
4461 return => {desc => 'Hold count', type => 'number'}
4465 # XXX Need to add type I holds to these counts
4466 sub rec_hold_count {
4467 my($self, $conn, $target_id, $args) = @_;
4474 filter => {metarecord => $target_id}
4481 filter => { id => $target_id },
4486 if($self->api_name =~ /mmr/) {
4487 delete $bre_join->{bre}->{filter};
4488 $bre_join->{bre}->{join} = $mmr_join;
4494 fkey => 'call_number',
4500 select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4504 cancel_time => undef,
4505 fulfillment_time => undef,
4509 hold_type => [qw/C F R/],
4512 select => {acp => ['id']},
4513 from => { acp => $cn_join }
4523 select => {acn => ['id']},
4524 from => {acn => $bre_join}
4534 select => {bmp => ['id']},
4535 from => {bmp => $bre_join}
4543 target => $target_id
4551 if($self->api_name =~ /mmr/) {
4552 $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4557 select => {bre => ['id']},
4558 from => {bre => $mmr_join}
4564 $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4567 target => $target_id
4573 if (my $pld = $args->{pickup_lib_descendant}) {
4575 my $top_ou = new_editor()->search_actor_org_unit(
4576 {parent_ou => undef}
4577 )->[0]; # XXX Assumes single root node. Not alone in this...
4579 $query->{where}->{'+ahr'}->{pickup_lib} = {
4581 select => {aou => [{
4583 transform => 'actor.org_unit_descendants',
4584 result_field => 'id'
4587 where => {id => $pld}
4589 } if ($pld != $top_ou->id);
4592 # To avoid Internal Server Errors, we get an editor, then run the
4593 # query and check the result. If anything fails, we'll return 0.
4595 if (my $e = new_editor()) {
4596 my $query_result = $e->json_query($query);
4597 if ($query_result && @{$query_result}) {
4598 $result = $query_result->[0]->{count}
4605 # A helper function to calculate a hold's expiration time at a given
4606 # org_unit. Takes the org_unit as an argument and returns either the
4607 # hold expire time as an ISO8601 string or undef if there is no hold
4608 # expiration interval set for the subject ou.
4609 sub calculate_expire_time
4612 my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4614 my $date = DateTime->now->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($interval));
4615 return $U->epoch2ISO8601($date->epoch);
4621 __PACKAGE__->register_method(
4622 method => 'mr_hold_filter_attrs',
4623 api_name => 'open-ils.circ.mmr.holds.filters',
4628 Returns the set of available formats and languages for the
4629 constituent records of the provided metarcord.
4630 If an array of hold IDs is also provided, information about
4631 each is returned as well. This information includes:
4632 1. a slightly easier to read version of holdable_formats
4633 2. attributes describing the set of format icons included
4634 in the set of desired, constituent records.
4637 {desc => 'Metarecord ID', type => 'number'},
4638 {desc => 'Context Org ID', type => 'number'},
4639 {desc => 'Hold ID List', type => 'array'},
4643 Stream of objects. The first will have a 'metarecord' key
4644 containing non-hold-specific metarecord information, subsequent
4645 responses will contain a 'hold' key containing hold-specific
4653 sub mr_hold_filter_attrs {
4654 my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4655 my $e = new_editor();
4657 # by default, return MR / hold attributes for all constituent
4658 # records with holdable copies. If there is a hard boundary,
4659 # though, limit to records with copies within the boundary,
4660 # since anything outside the boundary can never be held.
4663 $org_depth = $U->ou_ancestor_setting_value(
4664 $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4667 # get all org-scoped records w/ holdable copies for this metarecord
4668 my ($bre_ids) = $self->method_lookup(
4669 'open-ils.circ.holds.metarecord.filtered_records')->run(
4670 $mr_id, undef, $org_id, $org_depth);
4672 my $item_lang_attr = 'item_lang'; # configurable?
4673 my $format_attr = $e->retrieve_config_global_flag(
4674 'opac.metarecord.holds.format_attr')->value;
4676 # helper sub for fetching ccvms for a batch of record IDs
4677 sub get_batch_ccvms {
4678 my ($e, $attr, $bre_ids) = @_;
4679 return [] unless $bre_ids and @$bre_ids;
4680 my $vals = $e->search_metabib_record_attr_flat({
4684 return [] unless @$vals;
4685 return $e->search_config_coded_value_map({
4687 code => [map {$_->value} @$vals]
4691 my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4692 my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4697 formats => $formats,
4702 return unless $hold_ids;
4703 my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4704 $icon_attr = $icon_attr ? $icon_attr->value : '';
4706 for my $hold_id (@$hold_ids) {
4707 my $hold = $e->retrieve_action_hold_request($hold_id)
4708 or return $e->event;
4710 next unless $hold->hold_type eq 'M';
4720 # collect the ccvm's for the selected formats / language
4721 # (i.e. the holdable formats) on the MR.
4722 # this assumes a two-key structure for format / language,
4723 # though no assumption is made about the keys themselves.
4724 my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4726 my $format_vals = [];
4727 for my $val (values %$hformats) {
4728 # val is either a single ccvm or an array of them
4729 $val = [$val] unless ref $val eq 'ARRAY';
4730 for my $node (@$val) {
4731 push (@$lang_vals, $node->{_val})
4732 if $node->{_attr} eq $item_lang_attr;
4733 push (@$format_vals, $node->{_val})
4734 if $node->{_attr} eq $format_attr;
4738 # fetch the ccvm's for consistency with the {metarecord} blob
4739 $resp->{hold}{formats} = $e->search_config_coded_value_map({
4740 ctype => $format_attr, code => $format_vals});
4741 $resp->{hold}{langs} = $e->search_config_coded_value_map({
4742 ctype => $item_lang_attr, code => $lang_vals});
4744 # find all of the bib records within this metarcord whose
4745 # format / language match the holdable formats on the hold
4746 my ($bre_ids) = $self->method_lookup(
4747 'open-ils.circ.holds.metarecord.filtered_records')->run(
4748 $hold->target, $hold->holdable_formats,
4749 $hold->selection_ou, $hold->selection_depth);
4751 # now find all of the 'icon' attributes for the records
4752 $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4753 $client->respond($resp);
4759 __PACKAGE__->register_method(
4760 method => "copy_has_holds_count",
4761 api_name => "open-ils.circ.copy.has_holds_count",
4765 Returns the number of holds a paticular copy has
4768 { desc => 'Authentication Token', type => 'string'},
4769 { desc => 'Copy ID', type => 'number'}
4780 sub copy_has_holds_count {
4781 my( $self, $conn, $auth, $copyid ) = @_;
4782 my $e = new_editor(authtoken=>$auth);
4783 return $e->event unless $e->checkauth;
4785 if( $copyid && $copyid > 0 ) {
4786 my $meth = 'retrieve_action_has_holds_count';
4787 my $data = $e->$meth($copyid);
4789 return $data->count();
4795 __PACKAGE__->register_method(
4796 method => "retrieve_holds_by_usr_notify_value_staff",
4797 api_name => "open-ils.circ.holds.retrieve_by_notify_staff",
4799 desc => "Retrieve the hold, for the specified user using the notify value. $ses_is_req_note",
4801 { desc => 'Authentication token', type => 'string' },
4802 { desc => 'User ID', type => 'number' },
4803 { desc => 'notify value', type => 'string' },
4804 { desc => 'notify_type', type => 'string' }
4807 desc => 'Hold objects with transits attached, event on error',
4812 sub retrieve_holds_by_usr_notify_value_staff {
4814 my($self, $conn, $auth, $usr_id, $contact, $cType) = @_;
4816 my $e = new_editor(authtoken=>$auth);
4817 $e->checkauth or return $e->event;
4819 if ($e->requestor->id != $usr_id){
4820 $e->allowed('VIEW_HOLD') or return $e->event;
4824 "select" => { "ahr" => ["id", "sms_notify", "phone_notify", "email_notify", "sms_carrier"]},
4828 "capture_time" => undef,
4829 "cancel_time" => undef,
4830 "fulfillment_time" => undef,
4834 if ($cType eq "day_phone" or $cType eq "evening_phone" or
4835 $cType eq "other_phone" or $cType eq "default_phone"){
4836 $q->{where}->{"-not"} = [
4837 { "phone_notify" => { "=" => $contact} },
4838 { "phone_notify" => { "<>" => undef } }
4843 if ($cType eq "default_sms") {
4844 $q->{where}->{"-not"} = [
4845 { "sms_notify" => { "=" => $contact} },
4846 { "sms_notify" => { "<>" => undef } }
4850 if ($cType eq "default_sms_carrier_id") {
4851 $q->{where}->{"-not"} = [
4852 { "sms_carrier" => { "=" => int($contact)} },
4853 { "sms_carrier" => { "<>" => undef } }
4857 if ($cType =~ /notify/){
4858 # this is was notification pref change
4859 # we find all unfulfilled holds that match have that pref
4860 my $optr = $contact == 1 ? "<>" : "="; # unless it's email, true val means we want to query for not null
4861 my $conj = $optr eq '=' ? '-or' : '-and';
4862 if ($cType =~ /sms/) {
4863 $q->{where}->{$conj} = [ { sms_notify => { $optr => undef } }, { sms_notify => { $optr => '' } } ];
4865 if ($cType =~ /phone/) {
4866 $q->{where}->{$conj} = [ { phone_notify => { $optr => undef } }, { phone_notify => { $optr => '' } } ];
4868 if ($cType =~ /email/) {
4870 $q->{where}->{'+ahr'} = 'email_notify';
4872 $q->{where}->{'-not'} = {'+ahr' => 'email_notify'};
4877 my $holds = $e->json_query($q);
4878 #$hold_ids = [ map { $_->{id} } @$hold_ids ];
4883 __PACKAGE__->register_method(
4884 method => "batch_update_holds_by_value_staff",
4885 api_name => "open-ils.circ.holds.batch_update_holds_by_notify_staff",
4887 desc => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
4889 { desc => 'Authentication token', type => 'string' },
4890 { desc => 'User ID', type => 'number' },
4891 { desc => 'Hold IDs', type => 'array' },
4892 { desc => 'old notify value', type => 'string' },
4893 { desc => 'new notify value', type => 'string' },
4894 { desc => 'field name', type => 'string' },
4895 { desc => 'SMS carrier ID', type => 'number' }
4899 desc => 'Hold objects with transits attached, event on error',
4904 sub batch_update_holds_by_value_staff {
4905 my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType, $carrierId) = @_;
4907 my $e = new_editor(authtoken=>$auth, xact=>1);
4908 $e->checkauth or return $e->event;
4909 if ($e->requestor->id != $usr_id){
4910 $e->allowed('UPDATE_HOLD') or return $e->event;
4914 for my $id (@$hold_ids) {
4916 my $hold = $e->retrieve_action_hold_request($id);
4918 if ($cType eq "day_phone" or $cType eq "evening_phone" or
4919 $cType eq "other_phone" or $cType eq "default_phone") {
4921 if ($newval eq '') {
4922 $hold->clear_phone_notify();
4925 $hold->phone_notify($newval);
4929 if ($cType eq "default_sms"){
4930 if ($newval eq '') {
4931 $hold->clear_sms_notify();
4932 $hold->clear_sms_carrier(); # TODO: prevent orphan sms_carrier, via db trigger
4935 $hold->sms_notify($newval);
4936 $hold->sms_carrier($carrierId);
4941 if ($cType eq "default_sms_carrier_id") {
4942 $hold->sms_carrier($newval);
4945 if ($cType =~ /notify/){
4946 # this is a notification pref change
4947 if ($cType =~ /email/) { $hold->email_notify($newval); }
4948 if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
4949 if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
4950 # the other case, where x_notify is changed to true,
4951 # is covered by an actual value being assigned
4954 $e->update_action_hold_request($hold) or return $e->die_event;
4959 $e->commit; #unless $U->event_code($res);
4965 __PACKAGE__->register_method(
4966 method => "retrieve_holds_by_usr_with_notify",
4967 api_name => "open-ils.circ.holds.retrieve.by_usr.with_notify",
4969 desc => "Retrieve the hold, for the specified user using the notify value. $ses_is_req_note",
4971 { desc => 'Authentication token', type => 'string' },
4972 { desc => 'User ID', type => 'number' },
4975 desc => 'Lists of holds with notification values, event on error',
4980 sub retrieve_holds_by_usr_with_notify {
4982 my($self, $conn, $auth, $usr_id) = @_;
4984 my $e = new_editor(authtoken=>$auth);
4985 $e->checkauth or return $e->event;
4987 if ($e->requestor->id != $usr_id){
4988 $e->allowed('VIEW_HOLD') or return $e->event;
4992 "select" => { "ahr" => ["id", "phone_notify", "email_notify", "sms_carrier", "sms_notify"]},
4996 "capture_time" => undef,
4997 "cancel_time" => undef,
4998 "fulfillment_time" => undef,
5002 my $holds = $e->json_query($q);
5006 __PACKAGE__->register_method(
5007 method => "batch_update_holds_by_value",
5008 api_name => "open-ils.circ.holds.batch_update_holds_by_notify",
5010 desc => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
5012 { desc => 'Authentication token', type => 'string' },
5013 { desc => 'User ID', type => 'number' },
5014 { desc => 'Hold IDs', type => 'array' },
5015 { desc => 'old notify value', type => 'string' },
5016 { desc => 'new notify value', type => 'string' },
5017 { desc => 'notify_type', type => 'string' }
5020 desc => 'Hold objects with transits attached, event on error',
5025 sub batch_update_holds_by_value {
5026 my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType) = @_;
5028 my $e = new_editor(authtoken=>$auth, xact=>1);
5029 $e->checkauth or return $e->event;
5030 if ($e->requestor->id != $usr_id){
5031 $e->allowed('UPDATE_HOLD') or return $e->event;
5035 for my $id (@$hold_ids) {
5037 my $hold = $e->retrieve_action_hold_request(int($id));
5039 if ($cType eq "day_phone" or $cType eq "evening_phone" or
5040 $cType eq "other_phone" or $cType eq "default_phone") {
5041 # change phone number value on hold
5042 $hold->phone_notify($newval);
5044 if ($cType eq "default_sms") {
5045 # change SMS number value on hold
5046 $hold->sms_notify($newval);
5049 if ($cType eq "default_sms_carrier_id") {
5050 $hold->sms_carrier(int($newval));
5053 if ($cType =~ /notify/){
5054 # this is a notification pref change
5055 if ($cType =~ /email/) { $hold->email_notify($newval); }
5056 if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
5057 if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
5058 # the other case, where x_notify is changed to true,
5059 # is covered by an actual value being assigned
5062 $e->update_action_hold_request($hold) or return $e->die_event;
5067 $e->commit; #unless $U->event_code($res);
5071 __PACKAGE__->register_method(
5072 method => "hold_metadata",
5073 api_name => "open-ils.circ.hold.get_metadata",
5078 Returns a stream of objects containing whatever bib,
5079 volume, etc. data is available to the specific hold
5083 {desc => 'Hold Type', type => 'string'},
5084 {desc => 'Hold Target(s)', type => 'number or array'},
5085 {desc => 'Context org unit (optional)', type => 'number'}
5089 Stream of hold metadata objects.
5098 my ($self, $client, $hold_type, $hold_targets, $org_id) = @_;
5100 $hold_targets = [$hold_targets] unless ref $hold_targets;
5102 my $e = new_editor();
5103 for my $target (@$hold_targets) {
5105 # create a dummy hold for find_hold_mvr
5106 my $hold = Fieldmapper::action::hold_request->new;
5107 $hold->hold_type($hold_type);
5108 $hold->target($target);
5110 my (undef, $volume, $copy, $issuance, $part, $bre, $metarecord) =
5111 find_hold_mvr($e, $hold, {suppress_mvr => 1});
5113 $bre->clear_marc; # avoid bulk
5119 issuance => $issuance,
5123 metarecord => $metarecord,
5124 metarecord_filters => {}
5127 # If this is a bib hold or metarecord hold, also return the
5128 # available set of MR filters (AKA "Holdable Formats") for the
5129 # hold. For bib holds these may be used to upgrade the hold
5130 # from a bib to metarecord hold.
5131 if ($hold_type eq 'T') {
5132 my $map = $e->search_metabib_metarecord_source_map(
5133 {source => $meta->{bibrecord}->id})->[0];
5136 $meta->{metarecord} =
5137 $e->retrieve_metabib_metarecord($map->metarecord);
5140 # Also fetch the available parts for bib-level holds.
5141 $meta->{parts} = $e->search_biblio_monograph_part(
5142 {record => $bre->id, deleted => 'f'});
5145 if ($meta->{metarecord}) {
5148 $self->method_lookup('open-ils.circ.mmr.holds.filters')
5149 ->run($meta->{metarecord}->id, $org_id);
5152 $meta->{metarecord_filters} = $filters->{metarecord};
5156 $client->respond($meta);