1 # ---------------------------------------------------------------
2 # Copyright (C) 2005 Georgia Public Library Service
3 # Bill Erickson <highfalutin@gmail.com>
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 # ---------------------------------------------------------------
17 package OpenILS::Application::Circ::Holds;
18 use base qw/OpenILS::Application/;
19 use strict; use warnings;
20 use List::Util qw(shuffle);
21 use OpenILS::Application::AppUtils;
24 use OpenSRF::EX qw(:try);
28 use OpenSRF::Utils::Logger qw(:logger);
29 use OpenILS::Utils::CStoreEditor q/:funcs/;
30 use OpenILS::Utils::PermitHold;
31 use OpenSRF::Utils::SettingsClient;
32 use OpenILS::Const qw/:const/;
33 use OpenILS::Application::Circ::Transit;
34 use OpenILS::Application::Actor::Friends;
36 use DateTime::Format::ISO8601;
37 use OpenILS::Utils::DateTime qw/:datetime/;
38 use Digest::MD5 qw(md5_hex);
39 use OpenSRF::Utils::Cache;
40 use OpenSRF::Utils::JSON;
41 my $apputils = "OpenILS::Application::AppUtils";
44 __PACKAGE__->register_method(
45 method => "test_and_create_hold_batch",
46 api_name => "open-ils.circ.holds.test_and_create.batch",
49 desc => q/This is for batch creating a set of holds where every field is identical except for the targets./,
51 { desc => 'Authentication token', type => 'string' },
52 { desc => 'Hash of named parameters. Same as for open-ils.circ.title_hold.is_possible, though the pertinent target field is automatically populated based on the hold_type and the specified list of targets.', type => 'object'},
53 { desc => 'Array of target ids', type => 'array' }
56 desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
61 __PACKAGE__->register_method(
62 method => "test_and_create_hold_batch",
63 api_name => "open-ils.circ.holds.test_and_create.batch.override",
66 desc => '@see open-ils.circ.holds.test_and_create.batch',
71 sub test_and_create_hold_batch {
72 my( $self, $conn, $auth, $params, $target_list, $oargs ) = @_;
75 if ($self->api_name =~ /override/) {
77 $oargs = { all => 1 } unless defined $oargs;
78 $$params{oargs} = $oargs; # for is_possible checking.
81 my $e = new_editor(authtoken=>$auth);
82 return $e->die_event unless $e->checkauth;
83 $$params{'requestor'} = $e->requestor->id;
86 if ($$params{'hold_type'} eq 'T') { $target_field = 'titleid'; }
87 elsif ($$params{'hold_type'} eq 'C') { $target_field = 'copy_id'; }
88 elsif ($$params{'hold_type'} eq 'R') { $target_field = 'copy_id'; }
89 elsif ($$params{'hold_type'} eq 'F') { $target_field = 'copy_id'; }
90 elsif ($$params{'hold_type'} eq 'I') { $target_field = 'issuanceid'; }
91 elsif ($$params{'hold_type'} eq 'V') { $target_field = 'volume_id'; }
92 elsif ($$params{'hold_type'} eq 'M') { $target_field = 'mrid'; }
93 elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
94 else { return undef; }
96 my $formats_map = delete $$params{holdable_formats_map};
98 foreach (@$target_list) {
99 $$params{$target_field} = $_;
101 # copy the requested formats from the target->formats map
102 # into the top-level formats attr for each hold
103 $$params{holdable_formats} = $formats_map->{$_};
106 ($res) = $self->method_lookup(
107 'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
108 if ($res->{'success'} == 1 || ($override && $res->{place_unfillable})) {
110 $params->{'depth'} = $res->{'depth'} if $res->{'depth'};
112 if ($$oargs{honor_user_settings}) {
113 my $recipient = $e->retrieve_actor_user($$params{patronid})
114 or return $e->die_event;
115 my $opac_hold_notify = $e->search_actor_user_setting(
116 {usr => $$params{patronid}, name => 'opac.hold_notify'})->[0];
117 if ($opac_hold_notify) {
118 if ($opac_hold_notify->value =~ 'email') {
119 $$params{email_notify} = 1;
121 if ($opac_hold_notify->value =~ 'phone') {
122 my $opac_default_phone = $e->search_actor_user_setting(
123 {usr => $$params{patronid}, name => 'opac.default_phone'})->[0];
124 # FIXME - what's up with the ->value putting quotes around the string?
125 if ($opac_default_phone && $opac_default_phone->value =~ /^"(.*)"$/) {
126 $$params{phone_notify} = $1;
129 if ($opac_hold_notify->value =~ 'sms') {
130 my $opac_default_sms_carrier = $e->search_actor_user_setting(
131 {usr => $$params{patronid}, name => 'opac.default_sms_carrier'})->[0];
132 $$params{sms_carrier} = $opac_default_sms_carrier->value if $opac_default_sms_carrier;
133 my $opac_default_sms_notify = $e->search_actor_user_setting(
134 {usr => $$params{patronid}, name => 'opac.default_sms_notify'})->[0];
135 if ($opac_default_sms_notify && $opac_default_sms_notify->value =~ /^"(.*)"$/) {
136 $$params{sms_notify} = $1;
142 # Remove oargs from params so holds can be created.
143 if ($$params{oargs}) {
144 delete $$params{oargs};
147 my $ahr = construct_hold_request_object($params);
148 my ($res2) = $self->method_lookup(
150 ? 'open-ils.circ.holds.create.override'
151 : 'open-ils.circ.holds.create'
152 )->run($auth, $ahr, $oargs);
154 'target' => $$params{$target_field},
157 $conn->respond($res2);
160 'target' => $$params{$target_field},
163 $conn->respond($res);
169 __PACKAGE__->register_method(
170 method => "test_and_create_batch_hold_event",
171 api_name => "open-ils.circ.holds.test_and_create.subscription_batch",
174 desc => q/This is for batch creating a set of holds where every field is identical except for the target users./,
176 { desc => 'Authentication token', type => 'string' },
177 { desc => 'Hash of named parameters. Same as for open-ils.circ.title_hold.is_possible, though the pertinent target field is automatically populated based on the hold_type and the specified list of target users.', type => 'object'},
178 { desc => 'Container ID of the user bucket holding target users', type => 'number' },
179 { desc => 'target object ID (clarified by hold_type in param hash)', type => 'number' },
180 { desc => 'Randomize flag, set to 0 for "not randomized"', type => 'bool' }
183 desc => 'Stream of objects structured as {total=>X, count=>Y, target=>Z, patronid=>A, result=>$hold_id} on success, -1 on missing arg, event (or stream of events on "result" key of object) on error(s)',
188 __PACKAGE__->register_method(
189 method => "test_and_create_batch_hold_event",
190 api_name => "open-ils.circ.holds.test_and_create.subscription_batch.override",
193 desc => '@see open-ils.circ.holds.test_and_create.subscription_batch',
198 sub test_and_create_batch_hold_event {
199 my( $self, $conn, $auth, $params, $target_bucket, $target_id, $randomize, $oargs ) = @_;
202 $randomize //= 1; # default to random hold creation order
203 $$params{hold_type} //= 'T'; # default to title holds
206 if ($self->api_name =~ /override/) {
208 $oargs = { all => 1 } unless defined $oargs;
209 $$params{oargs} = $oargs; # for is_possible checking.
212 my $e = new_editor(authtoken=>$auth);
213 return $e->die_event unless $e->checkauth;
214 $$params{'requestor'} = $e->requestor->id;
217 my $org = $e->requestor->ws_ou || $e->requestor->home_ou;
218 # the perm locaiton shouldn't really matter here since holds
219 # will exist all over and MANAGE_HOLD_GROUPS should be universal
220 my $evt = $U->check_perms($e->requestor->id, $org, 'MANAGE_HOLD_GROUPS');
223 my $rand_setting = $U->ou_ancestor_setting_value($org, 'holds.subscription.randomize');
224 $randomize = $rand_setting if (defined $rand_setting);
227 if ($$params{'hold_type'} eq 'T') { $target_field = 'titleid'; }
228 elsif ($$params{'hold_type'} eq 'C') { $target_field = 'copy_id'; }
229 elsif ($$params{'hold_type'} eq 'R') { $target_field = 'copy_id'; }
230 elsif ($$params{'hold_type'} eq 'F') { $target_field = 'copy_id'; }
231 elsif ($$params{'hold_type'} eq 'I') { $target_field = 'issuanceid'; }
232 elsif ($$params{'hold_type'} eq 'V') { $target_field = 'volume_id'; }
233 elsif ($$params{'hold_type'} eq 'M') { $target_field = 'mrid'; }
234 elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
235 else { return undef; }
237 # Check for a valid record.
238 # XXX For now, because only title holds are allowed, we'll add only that check.
239 my $target_check = $e->json_query({
240 select => {bre => ['id']},
242 where => {deleted => 'f', id => $target_id}
244 return {error=>'invalid_target'} if (!@$target_check);
246 my $formats_map = delete($$params{holdable_formats_map}) || {};
248 my $target_list = $e->search_container_user_bucket_item({bucket => $target_bucket});
249 @$target_list = shuffle(@$target_list) if $randomize;
251 # Record the request...
253 my $bhe = Fieldmapper::action::batch_hold_event->new;
255 $bhe->staff($e->requestor->id);
256 $bhe->bucket($target_bucket);
257 $bhe->target($target_id);
258 $bhe->hold_type($$params{hold_type});
259 $bhe = $e->create_action_batch_hold_event($bhe) or return $e->die_event;
262 my $total = scalar(@$target_list);
264 $conn->respond({total => $total, count => $count});
266 my $triggers = OpenSRF::AppSession->connect('open-ils.trigger');
267 foreach (@$target_list) {
269 $$params{$target_field} = $target_id;
270 $$params{patronid} = $_->target_user;
272 my $usr = $e->retrieve_actor_user([
281 my $user_setting_map = {
282 map { $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) }
286 $$params{pickup_lib} = $$user_setting_map{'opac.default_pickup_location'} || $usr->home_ou;
288 if ($user_setting_map->{'opac.hold_notify'} =~ /email/) {
289 $$params{email_notify} = 1;
291 delete $$params{email_notify};
294 if ($user_setting_map->{'opac.default_phone'} && $user_setting_map->{'opac.hold_notify'} =~ /phone/) {
295 $$params{phone_notify} = $user_setting_map->{'opac.default_phone'};
297 delete $$params{phone_notify};
300 if ($user_setting_map->{'opac.default_sms_carrier'}
301 && $user_setting_map->{'opac.default_sms_notify'}
302 && $user_setting_map->{'opac.hold_notify'} =~ /sms/) {
303 $$params{sms_carrier} = $user_setting_map->{'opac.default_sms_carrier'};
304 $$params{sms_notify} = $user_setting_map->{'opac.default_sms_notify'};
306 delete $$params{sms_carrier};
307 delete $$params{sms_notify};
310 # copy the requested formats from the target->formats map
311 # into the top-level formats attr for each hold ... empty for now, T holds only
312 $$params{holdable_formats} = $formats_map->{$_};
315 ($res) = $self->method_lookup(
316 'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
317 if ($res->{'success'} == 1) {
319 $params->{'depth'} = $res->{'depth'} if $res->{'depth'};
321 # Remove oargs from params so holds can be created.
322 if ($$params{oargs}) {
323 delete $$params{oargs};
326 my $ahr = construct_hold_request_object($params);
327 my ($res2) = $self->method_lookup(
329 ? 'open-ils.circ.holds.create.override'
330 : 'open-ils.circ.holds.create'
331 )->run($auth, $ahr, $oargs);
333 total => $total, count => $count,
334 'patronid' => $$params{patronid},
335 'target' => $$params{$target_field},
338 $conn->respond($res2);
340 unless (ref($res2->{result})) { # success returns a hold id only
342 my $bhem = Fieldmapper::action::batch_hold_event_map->new;
344 $bhem->batch_hold_event($bhe->id);
345 $bhem->hold($res2->{result});
346 $e->create_action_batch_hold_event_map($bhem) or return $e->die_event;
349 my $hold = $e->retrieve_action_hold_request($bhem->hold);
350 $triggers->request('open-ils.trigger.event.autocreate', 'hold_request.success', $hold, $hold->pickup_lib);
355 total => $total, count => $count,
356 'target' => $$params{$target_field},
357 'failedpatronid' => $$params{patronid},
360 $conn->respond($res);
367 __PACKAGE__->register_method(
368 method => "rollback_batch_hold_event",
369 api_name => "open-ils.circ.holds.rollback.subscription_batch",
372 desc => q/This is for batch creating a set of holds where every field is identical except for the target users./,
374 { desc => 'Authentication token', type => 'string' },
375 { desc => 'Hold Group Event ID to roll back', type => 'number' },
378 desc => 'Stream of objects structured as {total=>X, count=>Y} on success, event on error',
383 sub rollback_batch_hold_event {
384 my( $self, $conn, $auth, $event_id ) = @_;
386 my $e = new_editor(authtoken=>$auth,xact=>1);
387 return $e->die_event unless $e->checkauth;
389 my $org = $e->requestor->ws_ou || $e->requestor->home_ou;
390 my $evt = $U->check_perms($e->requestor->id, $org, 'MANAGE_HOLD_GROUPS');
393 my $batch_event = $e->retrieve_action_batch_hold_event($event_id);
394 my $target_list = $e->search_action_batch_hold_event_map({batch_hold_event => $event_id});
396 my $total = scalar(@$target_list);
398 $conn->respond({total => $total, count => $count});
400 for my $target (@$target_list) {
402 $self->method_lookup('open-ils.circ.hold.cancel')->run($auth, $target->hold, 8);
403 $conn->respond({ total => $total, count => $count });
406 $batch_event->cancelled('now');
407 $e->update_action_batch_hold_event($batch_event);
412 sub construct_hold_request_object {
415 my $ahr = Fieldmapper::action::hold_request->new;
418 foreach my $field (keys %{ $params }) {
419 if ($field eq 'depth') { $ahr->selection_depth($$params{$field}); }
420 elsif ($field eq 'patronid') {
421 $ahr->usr($$params{$field}); }
422 elsif ($field eq 'titleid') { $ahr->target($$params{$field}); }
423 elsif ($field eq 'copy_id') { $ahr->target($$params{$field}); }
424 elsif ($field eq 'issuanceid') { $ahr->target($$params{$field}); }
425 elsif ($field eq 'volume_id') { $ahr->target($$params{$field}); }
426 elsif ($field eq 'mrid') { $ahr->target($$params{$field}); }
427 elsif ($field eq 'partid') { $ahr->target($$params{$field}); }
429 $ahr->$field($$params{$field});
435 __PACKAGE__->register_method(
436 method => "create_hold_batch",
437 api_name => "open-ils.circ.holds.create.batch",
440 desc => q/@see open-ils.circ.holds.create.batch/,
442 { desc => 'Authentication token', type => 'string' },
443 { desc => 'Array of hold objects', type => 'array' }
446 desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
451 __PACKAGE__->register_method(
452 method => "create_hold_batch",
453 api_name => "open-ils.circ.holds.create.override.batch",
456 desc => '@see open-ils.circ.holds.create.batch',
461 sub create_hold_batch {
462 my( $self, $conn, $auth, $hold_list, $oargs ) = @_;
463 (my $method = $self->api_name) =~ s/\.batch//og;
464 foreach (@$hold_list) {
465 my ($res) = $self->method_lookup($method)->run($auth, $_, $oargs);
466 $conn->respond($res);
472 __PACKAGE__->register_method(
473 method => "create_hold",
474 api_name => "open-ils.circ.holds.create",
476 desc => "Create a new hold for an item. From a permissions perspective, " .
477 "the login session is used as the 'requestor' of the hold. " .
478 "The hold recipient is determined by the 'usr' setting within the hold object. " .
479 'First we verify the requestor has holds request permissions. ' .
480 'Then we verify that the recipient is allowed to make the given hold. ' .
481 'If not, we see if the requestor has "override" capabilities. If not, ' .
482 'a permission exception is returned. If permissions allow, we cycle ' .
483 'through the set of holds objects and create. ' .
484 'If the recipient does not have permission to place multiple holds ' .
485 'on a single title and said operation is attempted, a permission ' .
486 'exception is returned',
488 { desc => 'Authentication token', type => 'string' },
489 { desc => 'Hold object for hold to be created',
490 type => 'object', class => 'ahr' }
493 desc => 'New ahr ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
498 __PACKAGE__->register_method(
499 method => "create_hold",
500 api_name => "open-ils.circ.holds.create.override",
501 notes => '@see open-ils.circ.holds.create',
503 desc => "If the recipient is not allowed to receive the requested hold, " .
504 "call this method to attempt the override",
506 { desc => 'Authentication token', type => 'string' },
508 desc => 'Hold object for hold to be created',
509 type => 'object', class => 'ahr'
513 desc => 'New hold (ahr) ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
519 my( $self, $conn, $auth, $hold, $oargs ) = @_;
520 return -1 unless $hold;
521 my $e = new_editor(authtoken=>$auth, xact=>1);
522 return $e->die_event unless $e->checkauth;
525 if ($self->api_name =~ /override/) {
527 $oargs = { all => 1 } unless defined $oargs;
532 my $requestor = $e->requestor;
533 my $recipient = $requestor;
535 if( $requestor->id ne $hold->usr ) {
536 # Make sure the requestor is allowed to place holds for
537 # the recipient if they are not the same people
538 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->die_event;
539 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->die_event;
542 # If the related org setting tells us to, block if patron privs have expired
543 my $expire_setting = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_BLOCK_HOLD_FOR_EXPIRED_PATRON);
544 if ($expire_setting) {
545 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
546 clean_ISO8601($recipient->expire_date));
548 push( @events, OpenILS::Event->new(
549 'PATRON_ACCOUNT_EXPIRED',
550 "payload" => {"fail_part" => "actor.usr.privs_expired"}
551 )) if( CORE::time > $expire->epoch ) ;
554 # Now make sure the recipient is allowed to receive the specified hold
555 my $porg = $recipient->home_ou;
556 my $rid = $e->requestor->id;
557 my $t = $hold->hold_type;
559 # See if a duplicate hold already exists
561 usr => $recipient->id,
563 fulfillment_time => undef,
564 target => $hold->target,
565 cancel_time => undef,
568 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
570 my $existing = $e->search_action_hold_request($sargs);
572 # See if the requestor has the CREATE_DUPLICATE_HOLDS perm.
573 my $can_dup = $e->allowed('CREATE_DUPLICATE_HOLDS', $recipient->home_ou);
574 # How many are allowed.
575 my $num_dups = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_MAX_DUPLICATE_HOLDS, $e) || 0;
576 push( @events, OpenILS::Event->new('HOLD_EXISTS'))
577 unless (($t eq 'T' || $t eq 'M') && $can_dup && scalar(@$existing) < $num_dups);
578 # Note: We check for @$existing < $num_dups because we're adding a hold with this call.
581 my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
582 push( @events, OpenILS::Event->new('HOLD_ITEM_CHECKED_OUT')) if $checked_out;
584 if ( $t eq OILS_HOLD_TYPE_METARECORD ) {
585 return $e->die_event unless $e->allowed('MR_HOLDS', $porg);
586 } elsif ( $t eq OILS_HOLD_TYPE_TITLE ) {
587 return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
588 } elsif ( $t eq OILS_HOLD_TYPE_VOLUME ) {
589 return $e->die_event unless $e->allowed('VOLUME_HOLDS', $porg);
590 } elsif ( $t eq OILS_HOLD_TYPE_MONOPART ) {
591 return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
592 } elsif ( $t eq OILS_HOLD_TYPE_ISSUANCE ) {
593 return $e->die_event unless $e->allowed('ISSUANCE_HOLDS', $porg);
594 } elsif ( $t eq OILS_HOLD_TYPE_COPY ) {
595 return $e->die_event unless $e->allowed('COPY_HOLDS', $porg);
596 } elsif ( $t eq OILS_HOLD_TYPE_FORCE || $t eq OILS_HOLD_TYPE_RECALL ) {
597 my $copy = $e->retrieve_asset_copy($hold->target)
598 or return $e->die_event;
599 if ( $t eq OILS_HOLD_TYPE_FORCE ) {
600 return $e->die_event unless $e->allowed('COPY_HOLDS_FORCE', $copy->circ_lib);
601 } elsif ( $t eq OILS_HOLD_TYPE_RECALL ) {
602 return $e->die_event unless $e->allowed('COPY_HOLDS_RECALL', $copy->circ_lib);
611 for my $evt (@events) {
613 my $name = $evt->{textcode};
614 if($oargs->{all} || grep { $_ eq $name } @{$oargs->{events}}) {
615 return $e->die_event unless $e->allowed("$name.override", $porg);
623 # Check for hold expiration in the past, and set it to empty string.
624 $hold->expire_time(undef) if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1);
626 # set the configured expire time
627 unless($hold->expire_time || $U->is_true($hold->frozen)) {
628 $hold->expire_time(calculate_expire_time($recipient->home_ou));
632 # if behind-the-desk pickup is supported at the hold pickup lib,
633 # set the value to the patron default, unless a value has already
634 # been applied. If it's not supported, force the value to false.
636 my $bdous = $U->ou_ancestor_setting_value(
638 'circ.holds.behind_desk_pickup_supported', $e);
641 if (!defined $hold->behind_desk) {
643 my $set = $e->search_actor_user_setting({
645 name => 'circ.holds_behind_desk'
648 $hold->behind_desk('t') if $set and
649 OpenSRF::Utils::JSON->JSON2perl($set->value);
652 # behind the desk not supported, force it to false
653 $hold->behind_desk('f');
656 $hold->requestor($e->requestor->id);
657 $hold->request_lib($e->requestor->ws_ou);
658 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
659 $hold = $e->create_action_hold_request($hold) or return $e->die_event;
663 $conn->respond_complete($hold->id);
665 $U->simplereq('open-ils.hold-targeter',
666 'open-ils.hold-targeter.target', {hold => $hold->id}
667 ) unless $U->is_true($hold->frozen);
672 # makes sure that a user has permission to place the type of requested hold
673 # returns the Perm exception if not allowed, returns undef if all is well
674 sub _check_holds_perm {
675 my($type, $user_id, $org_id) = @_;
679 $evt = $apputils->check_perms($user_id, $org_id, "MR_HOLDS" );
680 } elsif ($type eq "T") {
681 $evt = $apputils->check_perms($user_id, $org_id, "TITLE_HOLDS" );
682 } elsif($type eq "V") {
683 $evt = $apputils->check_perms($user_id, $org_id, "VOLUME_HOLDS");
684 } elsif($type eq "C") {
685 $evt = $apputils->check_perms($user_id, $org_id, "COPY_HOLDS" );
692 # tests if the given user is allowed to place holds on another's behalf
693 sub _check_request_holds_perm {
696 if (my $evt = $apputils->check_perms(
697 $user_id, $org_id, "REQUEST_HOLDS")) {
702 my $ses_is_req_note = 'The login session is the requestor. If the requestor is different from the user, ' .
703 'then the requestor must have VIEW_HOLD permissions';
705 __PACKAGE__->register_method(
706 method => "retrieve_holds_by_id",
707 api_name => "open-ils.circ.holds.retrieve_by_id",
709 desc => "Retrieve the hold, with hold transits attached, for the specified ID. $ses_is_req_note",
711 { desc => 'Authentication token', type => 'string' },
712 { desc => 'Hold ID', type => 'number' }
715 desc => 'Hold object with transits attached, event on error',
721 sub retrieve_holds_by_id {
722 my($self, $client, $auth, $hold_id) = @_;
723 my $e = new_editor(authtoken=>$auth);
724 $e->checkauth or return $e->event;
725 $e->allowed('VIEW_HOLD') or return $e->event;
727 my $holds = $e->search_action_hold_request(
729 { id => $hold_id , fulfillment_time => undef },
731 order_by => { ahr => "request_time" },
733 flesh_fields => {ahr => ['notes']}
738 flesh_hold_transits($holds);
739 flesh_hold_notices($holds, $e);
744 __PACKAGE__->register_method(
745 method => "retrieve_holds",
746 api_name => "open-ils.circ.holds.retrieve",
748 desc => "Retrieves all the holds, with hold transits attached, for the specified user. $ses_is_req_note",
750 { desc => 'Authentication token', type => 'string' },
751 { desc => 'User ID', type => 'integer' },
752 { desc => 'Available Only', type => 'boolean' }
755 desc => 'list of holds, event on error',
760 __PACKAGE__->register_method(
761 method => "retrieve_holds",
762 api_name => "open-ils.circ.holds.id_list.retrieve",
765 desc => "Retrieves all the hold IDs, for the specified user. $ses_is_req_note",
767 { desc => 'Authentication token', type => 'string' },
768 { desc => 'User ID', type => 'integer' },
769 { desc => 'Available Only', type => 'boolean' }
772 desc => 'list of holds, event on error',
777 __PACKAGE__->register_method(
778 method => "retrieve_holds",
779 api_name => "open-ils.circ.holds.canceled.retrieve",
782 desc => "Retrieves all the cancelled holds for the specified user. $ses_is_req_note",
784 { desc => 'Authentication token', type => 'string' },
785 { desc => 'User ID', type => 'integer' }
788 desc => 'list of holds, event on error',
793 __PACKAGE__->register_method(
794 method => "retrieve_holds",
795 api_name => "open-ils.circ.holds.canceled.id_list.retrieve",
798 desc => "Retrieves list of cancelled hold IDs for the specified user. $ses_is_req_note",
800 { desc => 'Authentication token', type => 'string' },
801 { desc => 'User ID', type => 'integer' }
804 desc => 'list of hold IDs, event on error',
811 my ($self, $client, $auth, $user_id, $available) = @_;
813 my $e = new_editor(authtoken=>$auth);
814 return $e->event unless $e->checkauth;
815 $user_id = $e->requestor->id unless defined $user_id;
817 my $notes_filter = {staff => 'f'};
818 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
819 unless($user_id == $e->requestor->id) {
820 if($e->allowed('VIEW_HOLD', $user->home_ou)) {
821 $notes_filter = {staff => 't'}
823 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
824 $e, $user_id, $e->requestor->id, 'hold.view');
825 return $e->event unless $allowed;
828 # staff member looking at his/her own holds can see staff and non-staff notes
829 $notes_filter = {} if $e->allowed('VIEW_HOLD', $user->home_ou);
833 select => {ahr => ['id']},
835 where => {usr => $user_id, fulfillment_time => undef}
838 if($self->api_name =~ /canceled/) {
840 $holds_query->{order_by} =
841 [{class => 'ahr', field => 'cancel_time', direction => 'desc'}];
843 recently_canceled_holds_filter($e, $holds_query);
847 # order non-cancelled holds by ready-for-pickup, then active, followed by suspended
848 # "compare" sorts false values to the front. testing pickup_lib != current_shelf_lib
849 # will sort by pl = csl > pl != csl > followed by csl is null;
850 $holds_query->{order_by} = [
852 field => 'pickup_lib',
853 compare => {'!=' => {'+ahr' => 'current_shelf_lib'}}},
854 {class => 'ahr', field => 'shelf_time'},
855 {class => 'ahr', field => 'frozen'},
856 {class => 'ahr', field => 'request_time'}
859 $holds_query->{where}->{cancel_time} = undef;
861 $holds_query->{where}->{shelf_time} = {'!=' => undef};
863 $holds_query->{where}->{pickup_lib} = {'=' => {'+ahr' => 'current_shelf_lib'}};
867 my $hold_ids = $e->json_query($holds_query);
868 $hold_ids = [ map { $_->{id} } @$hold_ids ];
870 return $hold_ids if $self->api_name =~ /id_list/;
873 for my $hold_id ( @$hold_ids ) {
875 my $hold = $e->retrieve_action_hold_request($hold_id);
876 $hold->notes($e->search_action_hold_request_note({hold => $hold_id, %$notes_filter}));
879 $e->search_action_hold_transit_copy([
880 {hold => $hold->id, cancel_time => undef},
881 {order_by => {ahtc => 'source_send_time desc'}, limit => 1}])->[0]
891 # Creates / augments a set of query filters to search for canceled holds
892 # based on circ.holds.canceled.* org settings.
893 sub recently_canceled_holds_filter {
894 my ($e, $filters) = @_;
896 $filters->{where} ||= {};
899 my $cancel_count = $U->ou_ancestor_setting_value(
900 $e->requestor->ws_ou, 'circ.holds.canceled.display_count', $e);
902 unless($cancel_count) {
903 $cancel_age = $U->ou_ancestor_setting_value(
904 $e->requestor->ws_ou, 'circ.holds.canceled.display_age', $e);
906 # if no settings are defined, default to last 10 cancelled holds
907 $cancel_count = 10 unless $cancel_age;
910 if($cancel_count) { # limit by count
912 $filters->{where}->{cancel_time} = {'!=' => undef};
913 $filters->{limit} = $cancel_count;
915 } elsif($cancel_age) { # limit by age
917 # find all of the canceled holds that were canceled within the configured time frame
918 my $date = DateTime->now->subtract(seconds =>
919 OpenILS::Utils::DateTime->interval_to_seconds($cancel_age));
921 $date = $U->epoch2ISO8601($date->epoch);
922 $filters->{where}->{cancel_time} = {'>=' => $date};
930 __PACKAGE__->register_method(
931 method => 'user_hold_count',
932 api_name => 'open-ils.circ.hold.user.count'
935 sub user_hold_count {
936 my ( $self, $conn, $auth, $userid ) = @_;
937 my $e = new_editor( authtoken => $auth );
938 return $e->event unless $e->checkauth;
939 my $patron = $e->retrieve_actor_user($userid)
941 return $e->event unless $e->allowed( 'VIEW_HOLD', $patron->home_ou );
942 return __user_hold_count( $self, $e, $userid );
945 sub __user_hold_count {
946 my ( $self, $e, $userid ) = @_;
947 my $holds = $e->search_action_hold_request(
950 fulfillment_time => undef,
951 cancel_time => undef,
956 return scalar(@$holds);
960 __PACKAGE__->register_method(
961 method => "retrieve_holds_by_pickup_lib",
962 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
964 "Retrieves all the holds, with hold transits attached, for the specified pickup_ou id."
967 __PACKAGE__->register_method(
968 method => "retrieve_holds_by_pickup_lib",
969 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
970 notes => "Retrieves all the hold ids for the specified pickup_ou id. "
973 sub retrieve_holds_by_pickup_lib {
974 my ($self, $client, $login_session, $ou_id) = @_;
976 #FIXME -- put an appropriate permission check here
977 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
978 # $login_session, $user_id, 'VIEW_HOLD' );
979 #return $evt if $evt;
981 my $holds = $apputils->simplereq(
983 "open-ils.cstore.direct.action.hold_request.search.atomic",
985 pickup_lib => $ou_id ,
986 fulfillment_time => undef,
989 { order_by => { ahr => "request_time" } }
992 if ( ! $self->api_name =~ /id_list/ ) {
993 flesh_hold_transits($holds);
997 return [ map { $_->id } @$holds ];
1001 __PACKAGE__->register_method(
1002 method => "uncancel_hold",
1003 api_name => "open-ils.circ.hold.uncancel"
1007 my($self, $client, $auth, $hold_id) = @_;
1008 my $e = new_editor(authtoken=>$auth, xact=>1);
1009 return $e->die_event unless $e->checkauth;
1011 my $hold = $e->retrieve_action_hold_request($hold_id)
1012 or return $e->die_event;
1013 return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
1015 if ($hold->fulfillment_time) {
1019 unless ($hold->cancel_time) {
1024 # if configured to reset the request time, also reset the expire time
1025 if($U->ou_ancestor_setting_value(
1026 $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
1028 $hold->request_time('now');
1029 $hold->expire_time(calculate_expire_time($hold->request_lib));
1032 $hold->clear_cancel_time;
1033 $hold->clear_cancel_cause;
1034 $hold->clear_cancel_note;
1035 $hold->clear_shelf_time;
1036 $hold->clear_current_copy;
1037 $hold->clear_capture_time;
1038 $hold->clear_prev_check_time;
1039 $hold->clear_shelf_expire_time;
1040 $hold->clear_current_shelf_lib;
1042 $e->update_action_hold_request($hold) or return $e->die_event;
1045 $U->simplereq('open-ils.hold-targeter',
1046 'open-ils.hold-targeter.target', {hold => $hold_id});
1052 __PACKAGE__->register_method(
1053 method => "cancel_hold",
1054 api_name => "open-ils.circ.hold.cancel",
1056 desc => 'Cancels the specified hold. The login session is the requestor. If the requestor is different from the usr field ' .
1057 'on the hold, the requestor must have CANCEL_HOLDS permissions. The hold may be either the hold object or the hold id',
1059 {desc => 'Authentication token', type => 'string'},
1060 {desc => 'Hold ID', type => 'number'},
1061 {desc => 'Cause of Cancellation', type => 'string'},
1062 {desc => 'Note', type => 'string'}
1065 desc => '1 on success, event on error'
1071 my($self, $client, $auth, $holdid, $cause, $note) = @_;
1073 my $e = new_editor(authtoken=>$auth, xact=>1);
1074 return $e->die_event unless $e->checkauth;
1076 my $hold = $e->retrieve_action_hold_request($holdid)
1077 or return $e->die_event;
1079 if( $e->requestor->id ne $hold->usr ) {
1080 return $e->die_event unless $e->allowed('CANCEL_HOLDS');
1083 if ($hold->cancel_time) {
1088 # If the hold is captured, reset the copy status
1089 if( $hold->capture_time and $hold->current_copy ) {
1091 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1092 or return $e->die_event;
1094 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1095 $logger->info("canceling hold $holdid whose item is on the holds shelf");
1096 # $logger->info("setting copy to status 'reshelving' on hold cancel");
1097 # $copy->status(OILS_COPY_STATUS_RESHELVING);
1098 # $copy->editor($e->requestor->id);
1099 # $copy->edit_date('now');
1100 # $e->update_asset_copy($copy) or return $e->event;
1102 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1104 my $hid = $hold->id;
1105 $logger->warn("! canceling hold [$hid] that is in transit");
1106 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
1109 my $trans = $e->retrieve_action_transit_copy($transid);
1110 # Leave the transit alive, but set the copy status to
1111 # reshelving so it will be properly reshelved when it gets back home
1113 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
1114 $e->update_action_transit_copy($trans) or return $e->die_event;
1120 $hold->cancel_time('now');
1121 $hold->cancel_cause($cause);
1122 $hold->cancel_note($note);
1123 $e->update_action_hold_request($hold)
1124 or return $e->die_event;
1128 # re-fetch the hold to pick up the real cancel_time (not "now") for A/T
1130 $hold = $e->retrieve_action_hold_request($hold->id) or return $e->die_event;
1133 if ($e->requestor->id == $hold->usr) {
1134 $U->create_events_for_hook('hold_request.cancel.patron', $hold, $hold->pickup_lib);
1136 $U->create_events_for_hook('hold_request.cancel.staff', $hold, $hold->pickup_lib);
1142 my $update_hold_desc = 'The login session is the requestor. ' .
1143 'If the requestor is different from the usr field on the hold, ' .
1144 'the requestor must have UPDATE_HOLDS permissions. ' .
1145 'If supplying a hash of hold data, "id" must be included. ' .
1146 'The hash is ignored if a hold object is supplied, ' .
1147 'so you should supply only one kind of hold data argument.' ;
1149 __PACKAGE__->register_method(
1150 method => "update_hold",
1151 api_name => "open-ils.circ.hold.update",
1153 desc => "Updates the specified hold. $update_hold_desc",
1155 {desc => 'Authentication token', type => 'string'},
1156 {desc => 'Hold Object', type => 'object'},
1157 {desc => 'Hash of values to be applied', type => 'object'}
1160 desc => 'Hold ID on success, event on error',
1166 __PACKAGE__->register_method(
1167 method => "batch_update_hold",
1168 api_name => "open-ils.circ.hold.update.batch",
1171 desc => "Updates the specified hold(s). $update_hold_desc",
1173 {desc => 'Authentication token', type => 'string'},
1174 {desc => 'Array of hold obejcts', type => 'array' },
1175 {desc => 'Array of hashes of values to be applied', type => 'array' }
1178 desc => 'Hold ID per success, event per error',
1184 my($self, $client, $auth, $hold, $values) = @_;
1185 my $e = new_editor(authtoken=>$auth, xact=>1);
1186 return $e->die_event unless $e->checkauth;
1187 my $resp = update_hold_impl($self, $e, $hold, $values);
1188 if ($U->event_code($resp)) {
1192 $e->commit; # FIXME: update_hold_impl already does $e->commit ??
1196 sub batch_update_hold {
1197 my($self, $client, $auth, $hold_list, $values_list) = @_;
1198 my $e = new_editor(authtoken=>$auth);
1199 return $e->die_event unless $e->checkauth;
1201 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.
1203 $values_list ||= []; # FIXME: either move this above $count declaration, or send an event if both lists undef. Probably the latter.
1205 # FIXME: Failing over to [] guarantees warnings for "Use of unitialized value" in update_hold_impl call.
1206 # FIXME: We should be sure we only call update_hold_impl with hold object OR hash, not both.
1208 for my $idx (0..$count-1) {
1210 my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
1211 $e->xact_commit unless $U->event_code($resp);
1212 $client->respond($resp);
1216 return undef; # not in the register return type, assuming we should always have at least one list populated
1219 sub update_hold_impl {
1220 my($self, $e, $hold, $values) = @_;
1222 my $need_retarget = 0;
1225 $hold = $e->retrieve_action_hold_request($values->{id})
1226 or return $e->die_event;
1227 for my $k (keys %$values) {
1228 # Outside of pickup_lib (covered by the first regex) I don't know when these would currently change.
1229 # But hey, why not cover things that may happen later?
1230 if ($k =~ '_(lib|ou)$' || $k eq 'target' || $k eq 'hold_type' || $k eq 'requestor' || $k eq 'selection_depth' || $k eq 'holdable_formats') {
1231 if (defined $values->{$k} && defined $hold->$k() && $values->{$k} ne $hold->$k()) {
1232 # Value changed? RETARGET!
1234 } elsif (defined $hold->$k() != defined $values->{$k}) {
1235 # Value being set or cleared? RETARGET!
1239 if (defined $values->{$k}) {
1240 $hold->$k($values->{$k});
1242 my $f = "clear_$k"; $hold->$f();
1247 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
1248 or return $e->die_event;
1250 # don't allow the user to be changed
1251 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
1253 if($hold->usr ne $e->requestor->id) {
1254 # if the hold is for a different user, make sure the
1255 # requestor has the appropriate permissions
1256 my $usr = $e->retrieve_actor_user($hold->usr)
1257 or return $e->die_event;
1258 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1262 # --------------------------------------------------------------
1263 # Changing the request time is like playing God
1264 # --------------------------------------------------------------
1265 if($hold->request_time ne $orig_hold->request_time) {
1266 return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
1267 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
1271 # --------------------------------------------------------------
1272 # Code for making sure staff have appropriate permissons for cut_in_line
1273 # This, as is, doesn't prevent a user from cutting their own holds in line
1275 # --------------------------------------------------------------
1276 if($U->is_true($hold->cut_in_line) ne $U->is_true($orig_hold->cut_in_line)) {
1277 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
1281 # --------------------------------------------------------------
1282 # Disallow hold suspencion if the hold is already captured.
1283 # --------------------------------------------------------------
1284 if ($U->is_true($hold->frozen) and not $U->is_true($orig_hold->frozen)) {
1285 $hold_status = _hold_status($e, $hold);
1286 if ($hold_status > 2 && $hold_status != 7) { # hold is captured
1287 $logger->info("bypassing hold freeze on captured hold");
1288 return OpenILS::Event->new('HOLD_SUSPEND_AFTER_CAPTURE');
1293 # --------------------------------------------------------------
1294 # if the hold is on the holds shelf or in transit and the pickup
1295 # lib changes we need to create a new transit.
1296 # --------------------------------------------------------------
1297 if($orig_hold->pickup_lib ne $hold->pickup_lib) {
1299 $hold_status = _hold_status($e, $hold) unless $hold_status;
1301 if($hold_status == 3) { # in transit
1303 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
1304 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
1306 $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
1308 # update the transit to reflect the new pickup location
1309 my $transit = $e->search_action_hold_transit_copy(
1310 {hold=>$hold->id, cancel_time => undef, dest_recv_time => undef})->[0]
1311 or return $e->die_event;
1313 $transit->prev_dest($transit->dest); # mark the previous destination on the transit
1314 $transit->dest($hold->pickup_lib);
1315 $e->update_action_hold_transit_copy($transit) or return $e->die_event;
1317 } elsif($hold_status == 4 or $hold_status == 5 or $hold_status == 8) { # on holds shelf
1319 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
1320 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
1322 $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
1324 if ($hold->pickup_lib eq $orig_hold->current_shelf_lib) {
1325 # This can happen if the pickup lib is changed while the hold is
1326 # on the shelf, then changed back to the original pickup lib.
1327 # Restore the original shelf_expire_time to prevent abuse.
1328 set_hold_shelf_expire_time(undef, $hold, $e, $hold->shelf_time);
1331 # clear to prevent premature shelf expiration
1332 $hold->clear_shelf_expire_time;
1334 # If a copy is targeted and pickup lib changes,
1335 # clear the current_copy so a retarget will re-evaluate
1336 # the hold from scratch.
1337 } elsif ($hold_status == 2) {
1338 $logger->info("Pickup location changed and waiting for capture, clear current_copy for hold ".$hold->id);
1339 $hold->clear_current_copy;
1343 if($U->is_true($hold->frozen)) {
1344 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1345 $hold->clear_current_copy;
1346 $hold->clear_prev_check_time;
1347 # Clear expire_time to prevent frozen holds from expiring.
1348 $logger->info("clearing expire_time for frozen hold ".$hold->id);
1349 $hold->clear_expire_time;
1352 # If the hold_expire_time is in the past && is not equal to the
1353 # original expire_time, then reset the expire time to be in the
1355 if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1 && $U->datecmp($hold->expire_time, $orig_hold->expire_time) != 0) {
1356 $hold->expire_time(calculate_expire_time($hold->request_lib));
1359 # If the hold is reactivated, reset the expire_time.
1360 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1361 $logger->info("Reset expire_time on activated hold ".$hold->id);
1362 $hold->expire_time(calculate_expire_time($hold->request_lib));
1365 $e->update_action_hold_request($hold) or return $e->die_event;
1368 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1369 $logger->info("Running targeter on activated hold ".$hold->id);
1370 $U->simplereq('open-ils.hold-targeter',
1371 'open-ils.hold-targeter.target', {hold => $hold->id});
1374 # a change to mint-condition changes the set of potential copies, so retarget the hold;
1375 if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
1376 _reset_hold($self, $e->requestor, $hold)
1377 } elsif($need_retarget && !defined $hold->capture_time()) { # If needed, retarget the hold due to changes
1378 $U->simplereq('open-ils.hold-targeter',
1379 'open-ils.hold-targeter.target', {hold => $hold->id});
1385 # this does not update the hold in the DB. It only
1386 # sets the shelf_expire_time field on the hold object.
1387 # start_time is optional and defaults to 'now'
1388 sub set_hold_shelf_expire_time {
1389 my ($class, $hold, $editor, $start_time) = @_;
1391 my $shelf_expire = $U->ou_ancestor_setting_value(
1393 'circ.holds.default_shelf_expire_interval',
1397 return undef unless $shelf_expire;
1399 $start_time = ($start_time) ?
1400 DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time)) :
1401 DateTime->now(time_zone => 'local'); # without time_zone we get UTC ... yuck!
1403 my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($shelf_expire);
1404 my $expire_time = $start_time->add(seconds => $seconds);
1406 # if the shelf expire time overlaps with a pickup lib's
1407 # closed date, push it out to the first open date
1408 my $dateinfo = $U->storagereq(
1409 'open-ils.storage.actor.org_unit.closed_date.overlap',
1410 $hold->pickup_lib, $expire_time->strftime('%FT%T%z'));
1413 my $dt_parser = DateTime::Format::ISO8601->new;
1414 $expire_time = $dt_parser->parse_datetime(clean_ISO8601($dateinfo->{end}));
1416 # TODO: enable/disable time bump via setting?
1417 $expire_time->set(hour => '23', minute => '59', second => '59');
1419 $logger->info("circulator: shelf_expire_time overlaps".
1420 " with closed date, pushing expire time to $expire_time");
1423 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
1429 my($e, $orig_hold, $hold, $copy) = @_;
1430 my $src = $orig_hold->pickup_lib;
1431 my $dest = $hold->pickup_lib;
1433 $logger->info("putting hold into transit on pickup_lib update");
1435 my $transit = Fieldmapper::action::hold_transit_copy->new;
1436 $transit->hold($hold->id);
1437 $transit->source($src);
1438 $transit->dest($dest);
1439 $transit->target_copy($copy->id);
1440 $transit->source_send_time('now');
1441 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1443 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1444 $copy->editor($e->requestor->id);
1445 $copy->edit_date('now');
1447 $e->create_action_hold_transit_copy($transit) or return $e->die_event;
1448 $e->update_asset_copy($copy) or return $e->die_event;
1452 # if the hold is frozen, this method ensures that the hold is not "targeted",
1453 # that is, it clears the current_copy and prev_check_time to essentiallly
1454 # reset the hold. If it is being activated, it runs the targeter in the background
1455 sub update_hold_if_frozen {
1456 my($self, $e, $hold, $orig_hold) = @_;
1457 return if $hold->capture_time;
1459 if($U->is_true($hold->frozen)) {
1460 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1461 $hold->clear_current_copy;
1462 $hold->clear_prev_check_time;
1465 if($U->is_true($orig_hold->frozen)) {
1466 $logger->info("Running targeter on activated hold ".$hold->id);
1467 $U->simplereq('open-ils.hold-targeter',
1468 'open-ils.hold-targeter.target', {hold => $hold->id});
1473 __PACKAGE__->register_method(
1474 method => "hold_note_CUD",
1475 api_name => "open-ils.circ.hold_request.note.cud",
1477 desc => 'Create, update or delete a hold request note. If the operator (from Auth. token) '
1478 . 'is not the owner of the hold, the UPDATE_HOLD permission is required',
1480 { desc => 'Authentication token', type => 'string' },
1481 { desc => 'Hold note object', type => 'object' }
1484 desc => 'Returns the note ID, event on error'
1490 my($self, $conn, $auth, $note) = @_;
1492 my $e = new_editor(authtoken => $auth, xact => 1);
1493 return $e->die_event unless $e->checkauth;
1495 my $hold = $e->retrieve_action_hold_request($note->hold)
1496 or return $e->die_event;
1498 if($hold->usr ne $e->requestor->id) {
1499 my $usr = $e->retrieve_actor_user($hold->usr);
1500 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1501 $note->staff('t') if $note->isnew;
1505 $e->create_action_hold_request_note($note) or return $e->die_event;
1506 } elsif($note->ischanged) {
1507 $e->update_action_hold_request_note($note) or return $e->die_event;
1508 } elsif($note->isdeleted) {
1509 $e->delete_action_hold_request_note($note) or return $e->die_event;
1517 __PACKAGE__->register_method(
1518 method => "retrieve_hold_status",
1519 api_name => "open-ils.circ.hold.status.retrieve",
1521 desc => 'Calculates the current status of the hold. The requestor must have ' .
1522 'VIEW_HOLD permissions if the hold is for a user other than the requestor' ,
1524 { desc => 'Hold ID', type => 'number' }
1527 # type => 'number', # event sometimes
1528 desc => <<'END_OF_DESC'
1529 Returns event on error or:
1530 -1 on error (for now),
1531 1 for 'waiting for copy to become available',
1532 2 for 'waiting for copy capture',
1535 5 for 'hold-shelf-delay'
1538 8 for 'captured, on wrong hold shelf'
1545 sub retrieve_hold_status {
1546 my($self, $client, $auth, $hold_id) = @_;
1548 my $e = new_editor(authtoken => $auth);
1549 return $e->event unless $e->checkauth;
1550 my $hold = $e->retrieve_action_hold_request($hold_id)
1551 or return $e->event;
1553 if( $e->requestor->id != $hold->usr ) {
1554 return $e->event unless $e->allowed('VIEW_HOLD');
1557 return _hold_status($e, $hold);
1563 if ($hold->cancel_time) {
1566 if ($U->is_true($hold->frozen) && !$hold->capture_time) {
1569 if ($hold->current_shelf_lib and $hold->current_shelf_lib ne $hold->pickup_lib) {
1572 if ($hold->fulfillment_time) {
1575 return 1 unless $hold->current_copy;
1576 return 2 unless $hold->capture_time;
1578 my $copy = $hold->current_copy;
1579 unless( ref $copy ) {
1580 $copy = $e->retrieve_asset_copy($hold->current_copy)
1581 or return $e->event;
1584 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
1586 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
1588 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
1589 return 4 unless $hs_wait_interval;
1591 # if a hold_shelf_status_delay interval is defined and start_time plus
1592 # the interval is greater than now, consider the hold to be in the virtual
1593 # "on its way to the holds shelf" status. Return 5.
1595 my $transit = $e->search_action_hold_transit_copy({
1597 target_copy => $copy->id,
1598 cancel_time => undef,
1599 dest_recv_time => {'!=' => undef},
1601 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
1602 $start_time = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time));
1603 my $end_time = $start_time->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($hs_wait_interval));
1605 return 5 if $end_time > DateTime->now;
1614 __PACKAGE__->register_method(
1615 method => "retrieve_hold_queue_stats",
1616 api_name => "open-ils.circ.hold.queue_stats.retrieve",
1618 desc => 'Returns summary data about the state of a hold',
1620 { desc => 'Authentication token', type => 'string'},
1621 { desc => 'Hold ID', type => 'number'},
1624 desc => q/Summary object with keys:
1625 total_holds : total holds in queue
1626 queue_position : current queue position
1627 potential_copies : number of potential copies for this hold
1628 estimated_wait : estimated wait time in days
1629 status : hold status
1630 -1 => error or unexpected state,
1631 1 => 'waiting for copy to become available',
1632 2 => 'waiting for copy capture',
1635 5 => 'hold-shelf-delay'
1642 sub retrieve_hold_queue_stats {
1643 my($self, $conn, $auth, $hold_id) = @_;
1644 my $e = new_editor(authtoken => $auth);
1645 return $e->event unless $e->checkauth;
1646 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
1647 if($e->requestor->id != $hold->usr) {
1648 return $e->event unless $e->allowed('VIEW_HOLD');
1650 return retrieve_hold_queue_status_impl($e, $hold);
1653 sub retrieve_hold_queue_status_impl {
1657 # The holds queue is defined as the distinct set of holds that share at
1658 # least one potential copy with the context hold, plus any holds that
1659 # share the same hold type and target. The latter part exists to
1660 # accomodate holds that currently have no potential copies
1661 my $q_holds = $e->json_query({
1663 # fetch cut_in_line and request_time since they're in the order_by
1664 # and we're asking for distinct values
1665 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1669 select => { ahcm => ['hold'] },
1674 'field' => 'target_copy',
1675 'fkey' => 'target_copy'
1679 where => { '+ahcm2' => { hold => $hold->id } },
1686 "field" => "cut_in_line",
1687 "transform" => "coalesce",
1689 "direction" => "desc"
1691 { "class" => "ahr", "field" => "request_time" }
1696 if (!@$q_holds) { # none? maybe we don't have a map ...
1697 $q_holds = $e->json_query({
1698 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1703 "field" => "cut_in_line",
1704 "transform" => "coalesce",
1706 "direction" => "desc"
1708 { "class" => "ahr", "field" => "request_time" }
1711 hold_type => $hold->hold_type,
1712 target => $hold->target,
1713 capture_time => undef,
1714 cancel_time => undef,
1716 {expire_time => undef },
1717 {expire_time => {'>' => 'now'}}
1725 for my $h (@$q_holds) {
1726 last if $h->{id} == $hold->id;
1730 my $hold_data = $e->json_query({
1732 acp => [ {column => 'id', transform => 'count', aggregate => 1, alias => 'count'} ],
1733 ccm => [ {column =>'avg_wait_time'} ]
1739 ccm => {type => 'left'}
1744 where => {'+ahcm' => {hold => $hold->id} }
1747 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1749 my $default_wait = $U->ou_ancestor_setting_value(
1750 $user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL, $e);
1751 my $min_wait = $U->ou_ancestor_setting_value(
1752 $user_org, 'circ.holds.min_estimated_wait_interval', $e);
1753 $min_wait = OpenILS::Utils::DateTime->interval_to_seconds($min_wait || '0 seconds');
1754 $default_wait ||= '0 seconds';
1756 # Estimated wait time is the average wait time across the set
1757 # of potential copies, divided by the number of potential copies
1758 # times the queue position.
1760 my $combined_secs = 0;
1761 my $num_potentials = 0;
1763 for my $wait_data (@$hold_data) {
1764 my $count += $wait_data->{count};
1765 $combined_secs += $count *
1766 OpenILS::Utils::DateTime->interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
1767 $num_potentials += $count;
1770 my $estimated_wait = -1;
1772 if($num_potentials) {
1773 my $avg_wait = $combined_secs / $num_potentials;
1774 $estimated_wait = $qpos * ($avg_wait / $num_potentials);
1775 $estimated_wait = $min_wait if $estimated_wait < $min_wait and $estimated_wait != -1;
1779 total_holds => scalar(@$q_holds),
1780 queue_position => $qpos,
1781 potential_copies => $num_potentials,
1782 status => _hold_status( $e, $hold ),
1783 estimated_wait => int($estimated_wait)
1788 sub fetch_open_hold_by_current_copy {
1791 my $hold = $apputils->simplereq(
1793 'open-ils.cstore.direct.action.hold_request.search.atomic',
1794 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1795 return $hold->[0] if ref($hold);
1799 sub fetch_related_holds {
1802 return $apputils->simplereq(
1804 'open-ils.cstore.direct.action.hold_request.search.atomic',
1805 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1809 __PACKAGE__->register_method(
1810 method => "hold_pull_list",
1811 api_name => "open-ils.circ.hold_pull_list.retrieve",
1813 desc => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1814 'The location is determined by the login session.',
1816 { desc => 'Limit (optional)', type => 'number'},
1817 { desc => 'Offset (optional)', type => 'number'},
1820 desc => 'reference to a list of holds, or event on failure',
1825 __PACKAGE__->register_method(
1826 method => "hold_pull_list",
1827 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
1829 desc => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
1830 'The location is determined by the login session.',
1832 { desc => 'Limit (optional)', type => 'number'},
1833 { desc => 'Offset (optional)', type => 'number'},
1836 desc => 'reference to a list of holds, or event on failure',
1841 __PACKAGE__->register_method(
1842 method => "hold_pull_list",
1843 api_name => "open-ils.circ.hold_pull_list.retrieve.count",
1845 desc => 'Returns a count of holds that need to be "pulled" by a given location. ' .
1846 'The location is determined by the login session.',
1848 { desc => 'Limit (optional)', type => 'number'},
1849 { desc => 'Offset (optional)', type => 'number'},
1852 desc => 'Holds count (integer), or event on failure',
1858 __PACKAGE__->register_method(
1859 method => "hold_pull_list",
1861 # TODO: tag with api_level 2 once fully supported
1862 api_name => "open-ils.circ.hold_pull_list.fleshed.stream",
1864 desc => q/Returns a stream of fleshed holds that need to be
1865 "pulled" by a given location. The location is
1866 determined by the login session.
1867 This API calls always run in authoritative mode./,
1869 { desc => 'Limit (optional)', type => 'number'},
1870 { desc => 'Offset (optional)', type => 'number'},
1873 desc => 'Stream of holds holds, or event on failure',
1878 sub hold_pull_list {
1879 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1880 my( $reqr, $evt ) = $U->checkses($authtoken);
1881 return $evt if $evt;
1883 my $org = $reqr->ws_ou || $reqr->home_ou;
1884 # the perm locaiton shouldn't really matter here since holds
1885 # will exist all over and VIEW_HOLDS should be universal
1886 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1887 return $evt if $evt;
1889 if($self->api_name =~ /count/) {
1891 my $count = $U->storagereq(
1892 'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1893 $org, $limit, $offset );
1895 $logger->info("Grabbing pull list for org unit $org with $count items");
1898 } elsif( $self->api_name =~ /id_list/ ) {
1900 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1901 $org, $limit, $offset );
1903 } elsif ($self->api_name =~ /fleshed/) {
1905 my $ids = $U->storagereq(
1906 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1907 $org, $limit, $offset );
1909 my $e = new_editor(xact => 1, requestor => $reqr);
1910 $conn->respond(uber_hold_impl($e, $_, {flesh_acpl => 1})) for @$ids;
1912 $conn->respond_complete;
1917 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1918 $org, $limit, $offset );
1922 __PACKAGE__->register_method(
1923 method => "print_hold_pull_list",
1924 api_name => "open-ils.circ.hold_pull_list.print",
1926 desc => 'Returns an HTML-formatted holds pull list',
1928 { desc => 'Authtoken', type => 'string'},
1929 { desc => 'Org unit ID. Optional, defaults to workstation org unit', type => 'number'},
1932 desc => 'HTML string',
1938 sub print_hold_pull_list {
1939 my($self, $client, $auth, $org_id) = @_;
1941 my $e = new_editor(authtoken=>$auth);
1942 return $e->event unless $e->checkauth;
1944 $org_id = (defined $org_id) ? $org_id : $e->requestor->ws_ou;
1945 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1947 my $hold_ids = $U->storagereq(
1948 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1951 return undef unless @$hold_ids;
1953 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1955 # Holds will /NOT/ be in order after this ...
1956 my $holds = $e->search_action_hold_request({id => $hold_ids}, {substream => 1});
1957 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1959 # ... so we must resort.
1960 my $hold_map = +{map { $_->id => $_ } @$holds};
1961 my $sorted_holds = [];
1962 push @$sorted_holds, $hold_map->{$_} foreach @$hold_ids;
1964 return $U->fire_object_event(
1965 undef, "ahr.format.pull_list", $sorted_holds,
1966 $org_id, undef, undef, $client
1971 __PACKAGE__->register_method(
1972 method => "print_hold_pull_list_stream",
1974 api_name => "open-ils.circ.hold_pull_list.print.stream",
1976 desc => 'Returns a stream of fleshed holds',
1978 { desc => 'Authtoken', type => 'string'},
1979 { 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)',
1984 desc => 'A stream of fleshed holds',
1990 sub print_hold_pull_list_stream {
1991 my($self, $client, $auth, $params) = @_;
1993 my $e = new_editor(authtoken=>$auth);
1994 return $e->die_event unless $e->checkauth;
1996 delete($$params{org_id}) unless (int($$params{org_id}));
1997 delete($$params{limit}) unless (int($$params{limit}));
1998 delete($$params{offset}) unless (int($$params{offset}));
1999 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2000 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2001 $$params{chunk_size} ||= 10;
2002 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2004 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2005 return $e->die_event unless $e->allowed('VIEW_HOLD', $$params{org_id });
2008 if ($$params{sort} && @{ $$params{sort} }) {
2009 for my $s (@{ $$params{sort} }) {
2010 if ($s eq 'acplo.position') {
2012 "class" => "acplo", "field" => "position",
2013 "transform" => "coalesce", "params" => [999]
2015 } elsif ($s eq 'prefix') {
2016 push @$sort, {"class" => "acnp", "field" => "label_sortkey"};
2017 } elsif ($s eq 'call_number') {
2018 push @$sort, {"class" => "acn", "field" => "label_sortkey"};
2019 } elsif ($s eq 'suffix') {
2020 push @$sort, {"class" => "acns", "field" => "label_sortkey"};
2021 } elsif ($s eq 'request_time') {
2022 push @$sort, {"class" => "ahr", "field" => "request_time"};
2026 push @$sort, {"class" => "ahr", "field" => "request_time"};
2029 my $holds_ids = $e->json_query(
2031 "select" => {"ahr" => ["id"]},
2036 "fkey" => "current_copy",
2038 "circ_lib" => $$params{org_id}, "status" => [0,7]
2043 "fkey" => "call_number",
2057 "fkey" => "circ_lib",
2060 "location" => {"=" => {"+acp" => "location"}}
2069 "capture_time" => undef,
2070 "cancel_time" => undef,
2072 {"expire_time" => undef },
2073 {"expire_time" => {">" => "now"}}
2077 (@$sort ? (order_by => $sort) : ()),
2078 ($$params{limit} ? (limit => $$params{limit}) : ()),
2079 ($$params{offset} ? (offset => $$params{offset}) : ())
2080 }, {"substream" => 1}
2081 ) or return $e->die_event;
2083 $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
2086 for my $hid (@$holds_ids) {
2087 push @chunk, $e->retrieve_action_hold_request([
2091 "ahr" => ["usr", "current_copy"],
2093 "acp" => ["location", "call_number", "parts"],
2094 "acn" => ["record","prefix","suffix"]
2099 if (@chunk >= $$params{chunk_size}) {
2100 $client->respond( \@chunk );
2104 $client->respond_complete( \@chunk ) if (@chunk);
2111 __PACKAGE__->register_method(
2112 method => 'fetch_hold_notify',
2113 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
2116 Returns a list of hold notification objects based on hold id.
2117 @param authtoken The loggin session key
2118 @param holdid The id of the hold whose notifications we want to retrieve
2119 @return An array of hold notification objects, event on error.
2123 sub fetch_hold_notify {
2124 my( $self, $conn, $authtoken, $holdid ) = @_;
2125 my( $requestor, $evt ) = $U->checkses($authtoken);
2126 return $evt if $evt;
2127 my ($hold, $patron);
2128 ($hold, $evt) = $U->fetch_hold($holdid);
2129 return $evt if $evt;
2130 ($patron, $evt) = $U->fetch_user($hold->usr);
2131 return $evt if $evt;
2133 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
2134 return $evt if $evt;
2136 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
2137 return $U->cstorereq(
2138 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
2142 __PACKAGE__->register_method(
2143 method => 'create_hold_notify',
2144 api_name => 'open-ils.circ.hold_notification.create',
2146 Creates a new hold notification object
2147 @param authtoken The login session key
2148 @param notification The hold notification object to create
2149 @return ID of the new object on success, Event on error
2153 sub create_hold_notify {
2154 my( $self, $conn, $auth, $note ) = @_;
2155 my $e = new_editor(authtoken=>$auth, xact=>1);
2156 return $e->die_event unless $e->checkauth;
2158 my $hold = $e->retrieve_action_hold_request($note->hold)
2159 or return $e->die_event;
2160 my $patron = $e->retrieve_actor_user($hold->usr)
2161 or return $e->die_event;
2163 return $e->die_event unless
2164 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
2166 $note->notify_staff($e->requestor->id);
2167 $e->create_action_hold_notification($note) or return $e->die_event;
2172 __PACKAGE__->register_method(
2173 method => 'create_hold_note',
2174 api_name => 'open-ils.circ.hold_note.create',
2176 Creates a new hold request note object
2177 @param authtoken The login session key
2178 @param note The hold note object to create
2179 @return ID of the new object on success, Event on error
2183 sub create_hold_note {
2184 my( $self, $conn, $auth, $note ) = @_;
2185 my $e = new_editor(authtoken=>$auth, xact=>1);
2186 return $e->die_event unless $e->checkauth;
2188 my $hold = $e->retrieve_action_hold_request($note->hold)
2189 or return $e->die_event;
2190 my $patron = $e->retrieve_actor_user($hold->usr)
2191 or return $e->die_event;
2193 return $e->die_event unless
2194 $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
2196 $e->create_action_hold_request_note($note) or return $e->die_event;
2201 __PACKAGE__->register_method(
2202 method => 'reset_hold',
2203 api_name => 'open-ils.circ.hold.reset',
2205 Un-captures and un-targets a hold, essentially returning
2206 it to the state it was in directly after it was placed,
2207 then attempts to re-target the hold
2208 @param authtoken The login session key
2209 @param holdid The id of the hold
2215 my( $self, $conn, $auth, $holdid ) = @_;
2217 my ($hold, $evt) = $U->fetch_hold($holdid);
2218 return $evt if $evt;
2219 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
2220 return $evt if $evt;
2221 $evt = _reset_hold($self, $reqr, $hold);
2222 return $evt if $evt;
2227 __PACKAGE__->register_method(
2228 method => 'reset_hold_batch',
2229 api_name => 'open-ils.circ.hold.reset.batch'
2232 sub reset_hold_batch {
2233 my($self, $conn, $auth, $hold_ids) = @_;
2235 my $e = new_editor(authtoken => $auth);
2236 return $e->event unless $e->checkauth;
2238 for my $hold_id ($hold_ids) {
2240 my $hold = $e->retrieve_action_hold_request(
2241 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
2242 or return $e->event;
2244 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
2245 _reset_hold($self, $e->requestor, $hold);
2253 my ($self, $reqr, $hold) = @_;
2255 my $e = new_editor(xact =>1, requestor => $reqr);
2257 $logger->info("reseting hold ".$hold->id);
2259 my $hid = $hold->id;
2261 if( $hold->capture_time and $hold->current_copy ) {
2263 my $copy = $e->retrieve_asset_copy($hold->current_copy)
2264 or return $e->die_event;
2266 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2267 $logger->info("setting copy to status 'reshelving' on hold retarget");
2268 $copy->status(OILS_COPY_STATUS_RESHELVING);
2269 $copy->editor($e->requestor->id);
2270 $copy->edit_date('now');
2271 $e->update_asset_copy($copy) or return $e->die_event;
2273 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
2275 $logger->warn("! reseting hold [$hid] that is in transit");
2276 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
2279 my $trans = $e->retrieve_action_transit_copy($transid);
2281 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
2282 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
2283 $logger->info("Transit abort completed with result $evt");
2284 unless ("$evt" eq 1) {
2293 $hold->clear_capture_time;
2294 $hold->clear_current_copy;
2295 $hold->clear_shelf_time;
2296 $hold->clear_shelf_expire_time;
2297 $hold->clear_current_shelf_lib;
2299 $e->update_action_hold_request($hold) or return $e->die_event;
2302 $U->simplereq('open-ils.hold-targeter',
2303 'open-ils.hold-targeter.target', {hold => $hold->id});
2309 __PACKAGE__->register_method(
2310 method => 'fetch_open_title_holds',
2311 api_name => 'open-ils.circ.open_holds.retrieve',
2313 Returns a list ids of un-fulfilled holds for a given title id
2314 @param authtoken The login session key
2315 @param id the id of the item whose holds we want to retrieve
2316 @param type The hold type - M, T, I, V, C, F, R
2320 sub fetch_open_title_holds {
2321 my( $self, $conn, $auth, $id, $type, $org ) = @_;
2322 my $e = new_editor( authtoken => $auth );
2323 return $e->event unless $e->checkauth;
2326 $org ||= $e->requestor->ws_ou;
2328 # return $e->search_action_hold_request(
2329 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
2331 # XXX make me return IDs in the future ^--
2332 my $holds = $e->search_action_hold_request(
2335 cancel_time => undef,
2337 fulfillment_time => undef
2341 flesh_hold_transits($holds);
2346 sub flesh_hold_transits {
2348 for my $hold ( @$holds ) {
2350 $apputils->simplereq(
2352 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
2353 { hold => $hold->id, cancel_time => undef },
2354 { order_by => { ahtc => 'id desc' }, limit => 1 }
2360 sub flesh_hold_notices {
2361 my( $holds, $e ) = @_;
2362 $e ||= new_editor();
2364 for my $hold (@$holds) {
2365 my $notices = $e->search_action_hold_notification(
2367 { hold => $hold->id },
2368 { order_by => { anh => 'notify_time desc' } },
2373 $hold->notify_count(scalar(@$notices));
2375 my $n = $e->retrieve_action_hold_notification($$notices[0])
2376 or return $e->event;
2377 $hold->notify_time($n->notify_time);
2383 __PACKAGE__->register_method(
2384 method => 'fetch_captured_holds',
2385 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
2389 Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
2390 @param authtoken The login session key
2391 @param org The org id of the location in question
2392 @param match_copy A specific copy to limit to
2396 __PACKAGE__->register_method(
2397 method => 'fetch_captured_holds',
2398 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
2402 Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
2403 @param authtoken The login session key
2404 @param org The org id of the location in question
2405 @param match_copy A specific copy to limit to
2409 __PACKAGE__->register_method(
2410 method => 'fetch_captured_holds',
2411 api_name => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
2415 Returns list ids of shelf-expired un-fulfilled holds for a given title id
2416 @param authtoken The login session key
2417 @param org The org id of the location in question
2418 @param match_copy A specific copy to limit to
2422 __PACKAGE__->register_method(
2423 method => 'fetch_captured_holds',
2425 'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
2429 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2430 for a given shelf lib
2434 __PACKAGE__->register_method(
2435 method => 'fetch_captured_holds',
2437 'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
2441 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2442 for a given shelf lib
2447 sub fetch_captured_holds {
2448 my( $self, $conn, $auth, $org, $match_copy ) = @_;
2450 my $e = new_editor(authtoken => $auth);
2451 return $e->die_event unless $e->checkauth;
2452 return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
2454 $org ||= $e->requestor->ws_ou;
2456 my $current_copy = { '!=' => undef };
2457 $current_copy = { '=' => $match_copy } if $match_copy;
2460 select => { alhr => ['id'] },
2465 fkey => 'current_copy'
2470 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
2472 capture_time => { "!=" => undef },
2473 current_copy => $current_copy,
2474 fulfillment_time => undef,
2475 current_shelf_lib => $org
2479 if($self->api_name =~ /expired/) {
2480 $query->{'where'}->{'+alhr'}->{'-or'} = {
2481 shelf_expire_time => { '<' => 'today'},
2482 cancel_time => { '!=' => undef },
2485 my $hold_ids = $e->json_query( $query );
2487 if ($self->api_name =~ /wrong_shelf/) {
2488 # fetch holds whose current_shelf_lib is $org, but whose pickup
2489 # lib is some other org unit. Ignore already-retrieved holds.
2491 pickup_lib_changed_on_shelf_holds(
2492 $e, $org, [map {$_->{id}} @$hold_ids]);
2493 # match the layout of other items in $hold_ids
2494 push (@$hold_ids, {id => $_}) for @$wrong_shelf;
2498 for my $hold_id (@$hold_ids) {
2499 if($self->api_name =~ /id_list/) {
2500 $conn->respond($hold_id->{id});
2504 $e->retrieve_action_hold_request([
2508 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
2509 order_by => {anh => 'notify_time desc'}
2519 __PACKAGE__->register_method(
2520 method => "print_expired_holds_stream",
2521 api_name => "open-ils.circ.captured_holds.expired.print.stream",
2525 sub print_expired_holds_stream {
2526 my ($self, $client, $auth, $params) = @_;
2528 # No need to check specific permissions: we're going to call another method
2529 # that will do that.
2530 my $e = new_editor("authtoken" => $auth);
2531 return $e->die_event unless $e->checkauth;
2533 delete($$params{org_id}) unless (int($$params{org_id}));
2534 delete($$params{limit}) unless (int($$params{limit}));
2535 delete($$params{offset}) unless (int($$params{offset}));
2536 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2537 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2538 $$params{chunk_size} ||= 10;
2539 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2541 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2543 my @hold_ids = $self->method_lookup(
2544 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
2545 )->run($auth, $params->{"org_id"});
2550 } elsif (defined $U->event_code($hold_ids[0])) {
2552 return $hold_ids[0];
2555 $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
2558 my @hid_chunk = splice @hold_ids, 0, $params->{"chunk_size"};
2560 my $result_chunk = $e->json_query({
2562 "acp" => ["barcode"],
2564 first_given_name second_given_name family_name alias
2573 "field" => "id", "fkey" => "current_copy",
2576 "field" => "id", "fkey" => "call_number",
2579 "field" => "id", "fkey" => "record"
2583 "acpl" => {"field" => "id", "fkey" => "location"}
2586 "au" => {"field" => "id", "fkey" => "usr"}
2589 "where" => {"+ahr" => {"id" => \@hid_chunk}}
2590 }) or return $e->die_event;
2591 $client->respond($result_chunk);
2598 __PACKAGE__->register_method(
2599 method => "check_title_hold_batch",
2600 api_name => "open-ils.circ.title_hold.is_possible.batch",
2603 desc => '@see open-ils.circ.title_hold.is_possible.batch',
2605 { desc => 'Authentication token', type => 'string'},
2606 { desc => 'Array of Hash of named parameters', type => 'array'},
2609 desc => 'Array of response objects',
2615 sub check_title_hold_batch {
2616 my($self, $client, $authtoken, $param_list, $oargs) = @_;
2617 foreach (@$param_list) {
2618 my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
2619 $client->respond($res);
2625 __PACKAGE__->register_method(
2626 method => "check_title_hold",
2627 api_name => "open-ils.circ.title_hold.is_possible",
2629 desc => 'Determines if a hold were to be placed by a given user, ' .
2630 'whether or not said hold would have any potential copies to fulfill it.' .
2631 'The named paramaters of the second argument include: ' .
2632 'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
2633 'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
2635 { desc => 'Authentication token', type => 'string'},
2636 { desc => 'Hash of named parameters', type => 'object'},
2639 desc => 'List of new message IDs (empty if none)',
2645 =head3 check_title_hold (token, hash)
2647 The named fields in the hash are:
2649 patronid - ID of the hold recipient (required)
2650 depth - hold range depth (default 0)
2651 pickup_lib - destination for hold, fallback value for selection_ou
2652 selection_ou - ID of org_unit establishing hard and soft hold boundary settings
2653 issuanceid - ID of the issuance to be held, required for Issuance level hold
2654 partid - ID of the monograph part to be held, required for monograph part level hold
2655 titleid - ID (BRN) of the title to be held, required for Title level hold
2656 volume_id - required for Volume level hold
2657 copy_id - required for Copy level hold
2658 mrid - required for Meta-record level hold
2659 hold_type - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record (default "T")
2661 All key/value pairs are passed on to do_possibility_checks.
2665 # FIXME: better params checking. what other params are required, if any?
2666 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
2667 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
2668 # used in conditionals, where it may be undefined, causing a warning.
2669 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
2671 sub check_title_hold {
2672 my( $self, $client, $authtoken, $params ) = @_;
2673 my $e = new_editor(authtoken=>$authtoken);
2674 return $e->event unless $e->checkauth;
2676 my %params = %$params;
2677 my $depth = $params{depth} || 0;
2678 $params{depth} = $depth; #define $params{depth} if unset, since it gets used later
2679 my $selection_ou = $params{selection_ou} || $params{pickup_lib};
2680 my $oargs = $params{oargs} || {};
2682 if($oargs->{events}) {
2683 @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
2687 my $patron = $e->retrieve_actor_user($params{patronid})
2688 or return $e->event;
2690 if( $e->requestor->id ne $patron->id ) {
2691 return $e->event unless
2692 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
2695 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
2697 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
2698 or return $e->event;
2700 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
2701 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
2704 my $return_depth = $hard_boundary; # default depth to return on success
2705 if(defined $soft_boundary and $depth < $soft_boundary) {
2706 # work up the tree and as soon as we find a potential copy, use that depth
2707 # also, make sure we don't go past the hard boundary if it exists
2709 # our min boundary is the greater of user-specified boundary or hard boundary
2710 my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
2711 $hard_boundary : $depth;
2713 my $depth = $soft_boundary;
2714 while($depth >= $min_depth) {
2715 $logger->info("performing hold possibility check with soft boundary $depth");
2716 @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
2718 $return_depth = $depth;
2723 } elsif(defined $hard_boundary and $depth < $hard_boundary) {
2724 # there is no soft boundary, enforce the hard boundary if it exists
2725 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
2726 @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
2728 # no boundaries defined, fall back to user specifed boundary or no boundary
2729 $logger->info("performing hold possibility check with no boundary");
2730 @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
2733 my $place_unfillable = 0;
2734 $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
2739 "depth" => $return_depth,
2740 "local_avail" => $status[1]
2742 } elsif ($status[2]) {
2743 my $n = scalar @{$status[2]};
2744 return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2746 return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2752 sub do_possibility_checks {
2753 my($e, $patron, $request_lib, $depth, %params) = @_;
2755 my $issuanceid = $params{issuanceid} || "";
2756 my $partid = $params{partid} || "";
2757 my $titleid = $params{titleid} || "";
2758 my $volid = $params{volume_id};
2759 my $copyid = $params{copy_id};
2760 my $mrid = $params{mrid} || "";
2761 my $pickup_lib = $params{pickup_lib};
2762 my $hold_type = $params{hold_type} || 'T';
2763 my $selection_ou = $params{selection_ou} || $pickup_lib;
2764 my $holdable_formats = $params{holdable_formats};
2765 my $oargs = $params{oargs} || {};
2772 if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
2774 return $e->event unless $copy = $e->retrieve_asset_copy($copyid);
2775 return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
2776 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2778 return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
2779 return verify_copy_for_hold(
2780 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
2783 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2785 return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
2786 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2788 return _check_volume_hold_is_possible(
2789 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2792 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
2794 return _check_title_hold_is_possible(
2795 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
2798 } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
2800 return _check_issuance_hold_is_possible(
2801 $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2804 } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
2806 return _check_monopart_hold_is_possible(
2807 $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2810 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2812 # pasing undef as the depth to filtered_records causes the depth
2813 # of the selection_ou to be used, which is not what we want here.
2816 my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
2818 for my $rec (@$recs) {
2819 @status = _check_title_hold_is_possible(
2820 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
2826 # else { Unrecognized hold_type ! } # FIXME: return error? or 0?
2829 sub MR_filter_records {
2836 my $opac_visible = shift;
2838 my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
2839 return $U->storagereq(
2840 'open-ils.storage.metarecord.filtered_records.atomic',
2841 $m, $f, $org_at_depth, $opac_visible
2844 __PACKAGE__->register_method(
2845 method => 'MR_filter_records',
2846 api_name => 'open-ils.circ.holds.metarecord.filtered_records',
2851 sub create_ranged_org_filter {
2852 my($e, $selection_ou, $depth) = @_;
2854 # find the orgs from which this hold may be fulfilled,
2855 # based on the selection_ou and depth
2857 my $top_org = $e->search_actor_org_unit([
2858 {parent_ou => undef},
2859 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
2862 return () if $depth == $top_org->ou_type->depth;
2864 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
2865 %org_filter = (circ_lib => []);
2866 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
2868 $logger->info("hold org filter at depth $depth and selection_ou ".
2869 "$selection_ou created list of @{$org_filter{circ_lib}}");
2875 sub _check_title_hold_is_possible {
2876 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
2877 # $holdable_formats is now unused. We pre-filter the MR's records.
2879 my $e = new_editor();
2881 # T holds on records that have parts are normally OK, but if the record has
2882 # no non-part copies, the hold will ultimately fail, so let's test for that.
2884 # If the global flag circ.holds.api_require_monographic_part_when_present is
2885 # enabled, then any configured parts for the bib is enough to disallow title holds.
2886 my $part_required = 0;
2887 my $parts = $e->search_biblio_monograph_part(
2893 my $part_required_flag = $e->retrieve_config_global_flag('circ.holds.api_require_monographic_part_when_present');
2894 $part_required = ($part_required_flag and $U->is_true($part_required_flag->enabled));
2895 if (!$part_required) {
2896 my $np_copies = $e->json_query({
2897 select => { acp => [{column => 'id', transform => 'count', alias => 'count'}]},
2898 from => {acp => {acn => {}, acpm => {type => 'left'}}},
2900 '+acp' => {deleted => 'f'},
2901 '+acn' => {deleted => 'f', record => $titleid},
2902 '+acpm' => {id => undef}
2905 $part_required = 1 if $np_copies->[0]->{count} == 0;
2908 if ($part_required) {
2909 $logger->info("title hold when monographic part required");
2913 "TITLE_HOLD_WHEN_MONOGRAPHIC_PART_REQUIRED",
2914 "payload" => {"fail_part" => "monographic_part_required"}
2920 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2922 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2923 my $copies = $e->json_query(
2925 select => { acp => ['id', 'circ_lib'] },
2930 fkey => 'call_number',
2931 filter => { record => $titleid }
2935 filter => { holdable => 't', deleted => 'f' },
2938 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' },
2939 acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2943 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2944 '+acpm' => { target_copy => undef } # ignore part-linked copies
2949 $logger->info("title possible found ".scalar(@$copies)." potential copies");
2953 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2954 "payload" => {"fail_part" => "no_ultimate_items"}
2959 # -----------------------------------------------------------------------
2960 # sort the copies into buckets based on their circ_lib proximity to
2961 # the patron's home_ou.
2962 # -----------------------------------------------------------------------
2964 my $home_org = $patron->home_ou;
2965 my $req_org = $request_lib->id;
2967 $prox_cache{$home_org} =
2968 $e->search_actor_org_unit_proximity({from_org => $home_org})
2969 unless $prox_cache{$home_org};
2970 my $home_prox = $prox_cache{$home_org};
2971 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2974 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2975 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2977 my @keys = sort { $a <=> $b } keys %buckets;
2980 if( $home_org ne $req_org ) {
2981 # -----------------------------------------------------------------------
2982 # shove the copies close to the request_lib into the primary buckets
2983 # directly before the farthest away copies. That way, they are not
2984 # given priority, but they are checked before the farthest copies.
2985 # -----------------------------------------------------------------------
2986 $prox_cache{$req_org} =
2987 $e->search_actor_org_unit_proximity({from_org => $req_org})
2988 unless $prox_cache{$req_org};
2989 my $req_prox = $prox_cache{$req_org};
2992 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2993 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2995 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2996 my $new_key = $highest_key - 0.5; # right before the farthest prox
2997 my @keys2 = sort { $a <=> $b } keys %buckets2;
2998 for my $key (@keys2) {
2999 last if $key >= $highest_key;
3000 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3004 @keys = sort { $a <=> $b } keys %buckets;
3009 my $age_protect_only = 0;
3010 OUTER: for my $key (@keys) {
3011 my @cps = @{$buckets{$key}};
3013 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3015 for my $copyid (@cps) {
3017 next if $seen{$copyid};
3018 $seen{$copyid} = 1; # there could be dupes given the merged buckets
3019 my $copy = $e->retrieve_asset_copy($copyid);
3020 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3022 unless($title) { # grab the title if we don't already have it
3023 my $vol = $e->retrieve_asset_call_number(
3024 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3025 $title = $vol->record;
3028 @status = verify_copy_for_hold(
3029 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3031 $age_protect_only ||= $status[3];
3032 last OUTER if $status[0];
3036 $status[3] = $age_protect_only;
3040 sub _check_issuance_hold_is_possible {
3041 my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3043 my $e = new_editor();
3044 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
3046 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
3047 my $copies = $e->json_query(
3049 select => { acp => ['id', 'circ_lib'] },
3055 filter => { issuance => $issuanceid }
3059 filter => { holdable => 't', deleted => 'f' },
3062 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3066 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3072 $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
3076 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3077 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3082 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3083 "payload" => {"fail_part" => "no_ultimate_items"}
3091 # -----------------------------------------------------------------------
3092 # sort the copies into buckets based on their circ_lib proximity to
3093 # the patron's home_ou.
3094 # -----------------------------------------------------------------------
3096 my $home_org = $patron->home_ou;
3097 my $req_org = $request_lib->id;
3099 $prox_cache{$home_org} =
3100 $e->search_actor_org_unit_proximity({from_org => $home_org})
3101 unless $prox_cache{$home_org};
3102 my $home_prox = $prox_cache{$home_org};
3103 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3106 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3107 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3109 my @keys = sort { $a <=> $b } keys %buckets;
3112 if( $home_org ne $req_org ) {
3113 # -----------------------------------------------------------------------
3114 # shove the copies close to the request_lib into the primary buckets
3115 # directly before the farthest away copies. That way, they are not
3116 # given priority, but they are checked before the farthest copies.
3117 # -----------------------------------------------------------------------
3118 $prox_cache{$req_org} =
3119 $e->search_actor_org_unit_proximity({from_org => $req_org})
3120 unless $prox_cache{$req_org};
3121 my $req_prox = $prox_cache{$req_org};
3124 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3125 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3127 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
3128 my $new_key = $highest_key - 0.5; # right before the farthest prox
3129 my @keys2 = sort { $a <=> $b } keys %buckets2;
3130 for my $key (@keys2) {
3131 last if $key >= $highest_key;
3132 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3136 @keys = sort { $a <=> $b } keys %buckets;
3141 my $age_protect_only = 0;
3142 OUTER: for my $key (@keys) {
3143 my @cps = @{$buckets{$key}};
3145 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3147 for my $copyid (@cps) {
3149 next if $seen{$copyid};
3150 $seen{$copyid} = 1; # there could be dupes given the merged buckets
3151 my $copy = $e->retrieve_asset_copy($copyid);
3152 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3154 unless($title) { # grab the title if we don't already have it
3155 my $vol = $e->retrieve_asset_call_number(
3156 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3157 $title = $vol->record;
3160 @status = verify_copy_for_hold(
3161 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3163 $age_protect_only ||= $status[3];
3164 last OUTER if $status[0];
3169 if (!defined($empty_ok)) {
3170 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3171 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3174 return (1,0) if ($empty_ok);
3176 $status[3] = $age_protect_only;
3180 sub _check_monopart_hold_is_possible {
3181 my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3183 my $e = new_editor();
3184 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
3186 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
3187 my $copies = $e->json_query(
3189 select => { acp => ['id', 'circ_lib'] },
3193 field => 'target_copy',
3195 filter => { part => $partid }
3199 filter => { holdable => 't', deleted => 'f' },
3202 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3206 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3212 $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
3216 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3217 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3222 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3223 "payload" => {"fail_part" => "no_ultimate_items"}
3231 # -----------------------------------------------------------------------
3232 # sort the copies into buckets based on their circ_lib proximity to
3233 # the patron's home_ou.
3234 # -----------------------------------------------------------------------
3236 my $home_org = $patron->home_ou;
3237 my $req_org = $request_lib->id;
3239 $prox_cache{$home_org} =
3240 $e->search_actor_org_unit_proximity({from_org => $home_org})
3241 unless $prox_cache{$home_org};
3242 my $home_prox = $prox_cache{$home_org};
3243 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3246 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3247 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3249 my @keys = sort { $a <=> $b } keys %buckets;
3252 if( $home_org ne $req_org ) {
3253 # -----------------------------------------------------------------------
3254 # shove the copies close to the request_lib into the primary buckets
3255 # directly before the farthest away copies. That way, they are not
3256 # given priority, but they are checked before the farthest copies.
3257 # -----------------------------------------------------------------------
3258 $prox_cache{$req_org} =
3259 $e->search_actor_org_unit_proximity({from_org => $req_org})
3260 unless $prox_cache{$req_org};
3261 my $req_prox = $prox_cache{$req_org};
3264 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3265 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3267 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
3268 my $new_key = $highest_key - 0.5; # right before the farthest prox
3269 my @keys2 = sort { $a <=> $b } keys %buckets2;
3270 for my $key (@keys2) {
3271 last if $key >= $highest_key;
3272 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3276 @keys = sort { $a <=> $b } keys %buckets;
3281 my $age_protect_only = 0;
3282 OUTER: for my $key (@keys) {
3283 my @cps = @{$buckets{$key}};
3285 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3287 for my $copyid (@cps) {
3289 next if $seen{$copyid};
3290 $seen{$copyid} = 1; # there could be dupes given the merged buckets
3291 my $copy = $e->retrieve_asset_copy($copyid);
3292 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3294 unless($title) { # grab the title if we don't already have it
3295 my $vol = $e->retrieve_asset_call_number(
3296 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3297 $title = $vol->record;
3300 @status = verify_copy_for_hold(
3301 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3303 $age_protect_only ||= $status[3];
3304 last OUTER if $status[0];
3309 if (!defined($empty_ok)) {
3310 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3311 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3314 return (1,0) if ($empty_ok);
3316 $status[3] = $age_protect_only;
3321 sub _check_volume_hold_is_possible {
3322 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3323 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
3324 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
3325 $logger->info("checking possibility of volume hold for volume ".$vol->id);
3327 my $filter_copies = [];
3328 for my $copy (@$copies) {
3329 # ignore part-mapped copies for regular volume level holds
3330 push(@$filter_copies, $copy) unless
3331 new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
3333 $copies = $filter_copies;
3338 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3339 "payload" => {"fail_part" => "no_ultimate_items"}
3345 my $age_protect_only = 0;
3346 for my $copy ( @$copies ) {
3347 @status = verify_copy_for_hold(
3348 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
3349 $age_protect_only ||= $status[3];
3352 $status[3] = $age_protect_only;
3358 sub verify_copy_for_hold {
3359 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
3360 # $oargs should be undef unless we're overriding.
3361 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
3362 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3365 requestor => $requestor,
3368 title_descriptor => $title->fixed_fields,
3369 pickup_lib => $pickup_lib,
3370 request_lib => $request_lib,
3372 show_event_list => 1
3376 # Check for override permissions on events.
3377 if ($oargs && $permitted && scalar @$permitted) {
3378 # Remove the events from permitted that we can override.
3379 if ($oargs->{events}) {
3380 foreach my $evt (@{$oargs->{events}}) {
3381 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3384 # Now, we handle the override all case by checking remaining
3385 # events against override permissions.
3386 if (scalar @$permitted && $oargs->{all}) {
3387 # Pre-set events and failed members of oargs to empty
3388 # arrays, if they are not set, yet.
3389 $oargs->{events} = [] unless ($oargs->{events});
3390 $oargs->{failed} = [] unless ($oargs->{failed});
3391 # When we're done with these checks, we swap permitted
3392 # with a reference to @disallowed.
3393 my @disallowed = ();
3394 foreach my $evt (@{$permitted}) {
3395 # Check if we've already seen the event in this
3396 # session and it failed.
3397 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3398 push(@disallowed, $evt);
3400 # We have to check if the requestor has the
3401 # override permission.
3403 # AppUtils::check_user_perms returns the perm if
3404 # the user doesn't have it, undef if they do.
3405 if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3406 push(@disallowed, $evt);
3407 push(@{$oargs->{failed}}, $evt->{textcode});
3409 push(@{$oargs->{events}}, $evt->{textcode});
3413 $permitted = \@disallowed;
3417 my $age_protect_only = 0;
3418 if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3419 $age_protect_only = 1;
3423 (not scalar @$permitted), # true if permitted is an empty arrayref
3424 ( # XXX This test is of very dubious value; someone should figure
3425 # out what if anything is checking this value
3426 ($copy->circ_lib == $pickup_lib) and
3427 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3436 sub find_nearest_permitted_hold {
3439 my $editor = shift; # CStoreEditor object
3440 my $copy = shift; # copy to target
3441 my $user = shift; # staff
3442 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3444 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3446 my $bc = $copy->barcode;
3448 # find any existing holds that already target this copy
3449 my $old_holds = $editor->search_action_hold_request(
3450 { current_copy => $copy->id,
3451 cancel_time => undef,
3452 capture_time => undef
3456 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3458 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3459 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3461 my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3463 # the nearest_hold API call now needs this
3464 $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3465 unless ref $copy->call_number;
3467 # search for what should be the best holds for this copy to fulfill
3468 my $best_holds = $U->storagereq(
3469 "open-ils.storage.action.hold_request.nearest_hold.atomic",
3470 $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3472 # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3474 for my $holdid (@$old_holds) {
3475 next unless $holdid;
3476 push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3480 unless(@$best_holds) {
3481 $logger->info("circulator: no suitable holds found for copy $bc");
3482 return (undef, $evt);
3488 # for each potential hold, we have to run the permit script
3489 # to make sure the hold is actually permitted.
3492 for my $holdid (@$best_holds) {
3493 next unless $holdid;
3494 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3496 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3497 # Force and recall holds bypass all rules
3498 if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3502 my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3503 my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3505 $reqr_cache{$hold->requestor} = $reqr;
3506 $org_cache{$hold->request_lib} = $rlib;
3508 # see if this hold is permitted
3509 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3511 patron_id => $hold->usr,
3514 pickup_lib => $hold->pickup_lib,
3515 request_lib => $rlib,
3527 unless( $best_hold ) { # no "good" permitted holds were found
3529 $logger->info("circulator: no suitable holds found for copy $bc");
3530 return (undef, $evt);
3533 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3535 # indicate a permitted hold was found
3536 return $best_hold if $check_only;
3538 # we've found a permitted hold. we need to "grab" the copy
3539 # to prevent re-targeted holds (next part) from re-grabbing the copy
3540 $best_hold->current_copy($copy->id);
3541 $editor->update_action_hold_request($best_hold)
3542 or return (undef, $editor->event);
3547 # re-target any other holds that already target this copy
3548 for my $old_hold (@$old_holds) {
3549 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3550 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3551 $old_hold->id." after a better hold [".$best_hold->id."] was found");
3552 $old_hold->clear_current_copy;
3553 $old_hold->clear_prev_check_time;
3554 $editor->update_action_hold_request($old_hold)
3555 or return (undef, $editor->event);
3556 push(@retarget, $old_hold->id);
3559 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3567 __PACKAGE__->register_method(
3568 method => 'all_rec_holds',
3569 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3573 my( $self, $conn, $auth, $title_id, $args ) = @_;
3575 my $e = new_editor(authtoken=>$auth);
3576 $e->checkauth or return $e->event;
3577 $e->allowed('VIEW_HOLD') or return $e->event;
3580 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
3581 $args->{cancel_time} = undef;
3584 metarecord_holds => []
3586 , volume_holds => []
3588 , recall_holds => []
3591 , issuance_holds => []
3594 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3596 $resp->{metarecord_holds} = $e->search_action_hold_request(
3597 { hold_type => OILS_HOLD_TYPE_METARECORD,
3598 target => $mr_map->metarecord,
3604 $resp->{title_holds} = $e->search_action_hold_request(
3606 hold_type => OILS_HOLD_TYPE_TITLE,
3607 target => $title_id,
3611 my $parts = $e->search_biblio_monograph_part(
3617 $resp->{part_holds} = $e->search_action_hold_request(
3619 hold_type => OILS_HOLD_TYPE_MONOPART,
3625 my $subs = $e->search_serial_subscription(
3626 { record_entry => $title_id }, {idlist=>1});
3629 my $issuances = $e->search_serial_issuance(
3630 {subscription => $subs}, {idlist=>1}
3634 $resp->{issuance_holds} = $e->search_action_hold_request(
3636 hold_type => OILS_HOLD_TYPE_ISSUANCE,
3637 target => $issuances,
3644 my $vols = $e->search_asset_call_number(
3645 { record => $title_id, deleted => 'f' }, {idlist=>1});
3647 return $resp unless @$vols;
3649 $resp->{volume_holds} = $e->search_action_hold_request(
3651 hold_type => OILS_HOLD_TYPE_VOLUME,
3656 my $copies = $e->search_asset_copy(
3657 { call_number => $vols, deleted => 'f' }, {idlist=>1});
3659 return $resp unless @$copies;
3661 $resp->{copy_holds} = $e->search_action_hold_request(
3663 hold_type => OILS_HOLD_TYPE_COPY,
3668 $resp->{recall_holds} = $e->search_action_hold_request(
3670 hold_type => OILS_HOLD_TYPE_RECALL,
3675 $resp->{force_holds} = $e->search_action_hold_request(
3677 hold_type => OILS_HOLD_TYPE_FORCE,
3685 __PACKAGE__->register_method(
3686 method => 'stream_wide_holds',
3689 api_name => 'open-ils.circ.hold.wide_hash.stream'
3692 sub stream_wide_holds {
3693 my($self, $client, $auth, $restrictions, $order_by, $limit, $offset, $options) = @_;
3696 my $e = new_editor(authtoken=>$auth);
3697 $e->checkauth or return $e->event;
3698 $e->allowed('VIEW_HOLD') or return $e->event;
3700 if ($options->{recently_canceled}) {
3701 # Map the the recently canceled holds filter into values
3702 # wide-stream understands.
3703 my $filter = recently_canceled_holds_filter($e);
3704 $restrictions->{$_} =
3705 $filter->{where}->{$_} for keys %{$filter->{where}};
3707 $limit = $filter->{limit} if $filter->{limit};
3710 my $filters = OpenSRF::Utils::JSON->perl2JSON($restrictions);
3711 $logger->info("WIDE HOLD FILTERS: $filters");
3713 my $st = OpenSRF::AppSession->create('open-ils.storage');
3714 my $req = $st->request(
3715 'open-ils.storage.action.live_holds.wide_hash.atomic',
3716 $restrictions, $order_by, $limit, $offset
3719 my $results = $req->recv;
3724 if(UNIVERSAL::isa($results,"Error")) {
3725 throw OpenSRF::EX::ERROR ("Error fetch hold shelf list");
3728 my @rows = @{ $results->content };
3730 # Force immediate send of count response
3731 my $mbc = $client->max_bundle_count;
3732 $client->max_bundle_count(1);
3733 $client->respond(shift @rows);
3734 $client->max_bundle_count($mbc);
3736 foreach my $hold (@rows) {
3737 $client->respond($hold) if $hold;
3740 $client->respond_complete;
3746 __PACKAGE__->register_method(
3747 method => 'uber_hold',
3749 api_name => 'open-ils.circ.hold.details.retrieve'
3753 my($self, $client, $auth, $hold_id, $args) = @_;
3754 my $e = new_editor(authtoken=>$auth);
3755 $e->checkauth or return $e->event;
3756 return uber_hold_impl($e, $hold_id, $args);
3759 __PACKAGE__->register_method(
3760 method => 'batch_uber_hold',
3763 api_name => 'open-ils.circ.hold.details.batch.retrieve'
3766 sub batch_uber_hold {
3767 my($self, $client, $auth, $hold_ids, $args) = @_;
3768 my $e = new_editor(authtoken=>$auth);
3769 $e->checkauth or return $e->event;
3770 $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3774 sub uber_hold_impl {
3775 my($e, $hold_id, $args) = @_;
3778 my $flesh_fields = ['current_copy', 'usr', 'notes'];
3779 push (@$flesh_fields, 'requestor') if $args->{include_requestor};
3780 push (@$flesh_fields, 'cancel_cause') if $args->{include_cancel_cause};
3781 push (@$flesh_fields, 'sms_carrier') if $args->{include_sms_carrier};
3783 my $hold = $e->retrieve_action_hold_request([
3785 {flesh => 1, flesh_fields => {ahr => $flesh_fields}}
3786 ]) or return $e->event;
3788 if($hold->usr->id ne $e->requestor->id) {
3789 # caller is asking for someone else's hold
3790 $e->allowed('VIEW_HOLD') or return $e->event;
3791 $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3792 [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3795 # caller is asking for own hold, but may not have permission to view staff notes
3796 unless($e->allowed('VIEW_HOLD')) {
3797 $hold->notes( # filter out any staff notes (unless marked as public)
3798 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3802 my $user = $hold->usr;
3803 $hold->usr($user->id);
3806 my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args);
3808 flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3809 flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3811 my $details = retrieve_hold_queue_status_impl($e, $hold);
3812 $hold->usr($user) if $args->{include_usr}; # re-flesh
3817 ($copy ? (copy => $copy) : ()),
3818 ($volume ? (volume => $volume) : ()),
3819 ($issuance ? (issuance => $issuance) : ()),
3820 ($part ? (part => $part) : ()),
3821 ($args->{include_bre} ? (bre => $bre) : ()),
3822 ($args->{suppress_mvr} ? () : (mvr => $mvr)),
3826 $resp->{copy}->location(
3827 $e->retrieve_asset_copy_location($resp->{copy}->location))
3828 if $resp->{copy} and $args->{flesh_acpl};
3830 unless($args->{suppress_patron_details}) {
3831 my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3832 $resp->{patron_first} = $user->first_given_name,
3833 $resp->{patron_last} = $user->family_name,
3834 $resp->{patron_barcode} = $card->barcode,
3835 $resp->{patron_alias} = $user->alias,
3843 # -----------------------------------------------------
3844 # Returns the MVR object that represents what the
3846 # -----------------------------------------------------
3848 my( $e, $hold, $args ) = @_;
3856 my $no_mvr = $args->{suppress_mvr};
3858 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3859 $metarecord = $e->retrieve_metabib_metarecord($hold->target)
3860 or return $e->event;
3861 $tid = $metarecord->master_record;
3863 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3864 $tid = $hold->target;
3866 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3867 $volume = $e->retrieve_asset_call_number($hold->target)
3868 or return $e->event;
3869 $tid = $volume->record;
3871 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3872 $issuance = $e->retrieve_serial_issuance([
3874 {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3875 ]) or return $e->event;
3877 $tid = $issuance->subscription->record_entry;
3879 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3880 $part = $e->retrieve_biblio_monograph_part([
3882 ]) or return $e->event;
3884 $tid = $part->record;
3886 } 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 ) {
3887 $copy = $e->retrieve_asset_copy([
3889 {flesh => 1, flesh_fields => {acp => ['call_number']}}
3890 ]) or return $e->event;
3892 $volume = $copy->call_number;
3893 $tid = $volume->record;
3896 if(!$copy and ref $hold->current_copy ) {
3897 $copy = $hold->current_copy;
3898 $hold->current_copy($copy->id) unless $args->{include_current_copy};
3901 if(!$volume and $copy) {
3902 $volume = $e->retrieve_asset_call_number($copy->call_number);
3905 # TODO return metarcord mvr for M holds
3906 my $title = $e->retrieve_biblio_record_entry($tid);
3907 return ( ($no_mvr) ? undef : $U->record_to_mvr($title),
3908 $volume, $copy, $issuance, $part, $title, $metarecord);
3911 __PACKAGE__->register_method(
3912 method => 'clear_shelf_cache',
3913 api_name => 'open-ils.circ.hold.clear_shelf.get_cache',
3917 Returns the holds processed with the given cache key
3922 sub clear_shelf_cache {
3923 my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3924 my $e = new_editor(authtoken => $auth, xact => 1);
3925 return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3928 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3930 my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3933 $logger->info("no hold data found in cache"); # XXX TODO return event
3939 foreach (keys %$hold_data) {
3940 $maximum += scalar(@{ $hold_data->{$_} });
3942 $client->respond({"maximum" => $maximum, "progress" => 0});
3944 for my $action (sort keys %$hold_data) {
3945 while (@{$hold_data->{$action}}) {
3946 my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3948 my $result_chunk = $e->json_query({
3950 "acp" => ["barcode"],
3952 first_given_name second_given_name family_name alias
3955 "acnp" => [{column => "label", alias => "prefix"}],
3956 "acns" => [{column => "label", alias => "suffix"}],
3964 "field" => "id", "fkey" => "current_copy",
3967 "field" => "id", "fkey" => "call_number",
3970 "field" => "id", "fkey" => "record"
3973 "field" => "id", "fkey" => "prefix"
3976 "field" => "id", "fkey" => "suffix"
3980 "acpl" => {"field" => "id", "fkey" => "location"}
3983 "au" => {"field" => "id", "fkey" => "usr"}
3986 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3987 }, {"substream" => 1}) or return $e->die_event;
3991 +{"action" => $action, "hold_details" => $_}
4002 __PACKAGE__->register_method(
4003 method => 'clear_shelf_process',
4005 api_name => 'open-ils.circ.hold.clear_shelf.process',
4008 1. Find all holds that have expired on the holds shelf
4010 3. If a clear-shelf status is configured, put targeted copies into this status
4011 4. Divide copies into 3 groups: items to transit, items to reshelve, and items
4012 that are needed for holds. No subsequent action is taken on the holds
4013 or items after grouping.
4018 sub clear_shelf_process {
4019 my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
4021 my $e = new_editor(authtoken=>$auth);
4022 $e->checkauth or return $e->die_event;
4023 my $cache = OpenSRF::Utils::Cache->new('global');
4025 $org_id ||= $e->requestor->ws_ou;
4026 $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
4028 my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
4030 my @hold_ids = $self->method_lookup(
4031 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
4032 )->run($auth, $org_id, $match_copy);
4037 my @canceled_holds; # newly canceled holds
4038 $chunk_size ||= 25; # chunked status updates
4039 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
4042 for my $hold_id (@hold_ids) {
4044 $logger->info("Clear shelf processing hold $hold_id");
4046 my $hold = $e->retrieve_action_hold_request([
4049 flesh_fields => {ahr => ['current_copy']}
4053 if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
4054 $hold->cancel_time('now');
4055 $hold->cancel_cause(2); # Hold Shelf expiration
4056 $e->update_action_hold_request($hold) or return $e->die_event;
4057 push(@canceled_holds, $hold_id);
4060 my $copy = $hold->current_copy;
4062 if($copy_status or $copy_status == 0) {
4063 # if a clear-shelf copy status is defined, update the copy
4064 $copy->status($copy_status);
4065 $copy->edit_date('now');
4066 $copy->editor($e->requestor->id);
4067 $e->update_asset_copy($copy) or return $e->die_event;
4070 push(@holds, $hold);
4071 $client->respond({maximum => int(scalar(@holds)), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
4080 pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
4083 for my $hold (@holds) {
4085 my $copy = $hold->current_copy;
4086 my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
4088 if($alt_hold and !$match_copy) {
4090 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
4092 } elsif($copy->circ_lib != $e->requestor->ws_ou) {
4094 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
4098 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
4102 my $cache_key = md5_hex(time . $$ . rand());
4103 $logger->info("clear_shelf_cache: storing under $cache_key");
4104 $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours. configurable?
4106 # tell the client we're done
4107 $client->respond_complete({cache_key => $cache_key});
4110 # fire off the hold cancelation trigger and wait for response so don't flood the service
4112 # refetch the holds to pick up the caclulated cancel_time,
4113 # which may be needed by Action/Trigger
4115 my $updated_holds = [];
4116 $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
4119 $U->create_events_for_hook(
4120 'hold_request.cancel.expire_holds_shelf',
4121 $_, $org_id, undef, undef, 1) for @$updated_holds;
4124 # tell the client we're done
4125 $client->respond_complete;
4129 # returns IDs for holds that are on the holds shelf but
4130 # have had their pickup_libs change while on the shelf.
4131 sub pickup_lib_changed_on_shelf_holds {
4134 my $ignore_holds = shift;
4135 $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
4138 select => { alhr => ['id'] },
4143 fkey => 'current_copy'
4148 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
4150 capture_time => { "!=" => undef },
4151 fulfillment_time => undef,
4152 current_shelf_lib => $org_id,
4153 pickup_lib => {'!=' => {'+alhr' => 'current_shelf_lib'}}
4158 $query->{where}->{'+alhr'}->{id} =
4159 {'not in' => $ignore_holds} if @$ignore_holds;
4161 my $hold_ids = $e->json_query($query);
4162 return [ map { $_->{id} } @$hold_ids ];
4165 __PACKAGE__->register_method(
4166 method => 'usr_hold_summary',
4167 api_name => 'open-ils.circ.holds.user_summary',
4169 Returns a summary of holds statuses for a given user
4173 sub usr_hold_summary {
4174 my($self, $conn, $auth, $user_id) = @_;
4176 my $e = new_editor(authtoken=>$auth);
4177 $e->checkauth or return $e->event;
4178 $e->allowed('VIEW_HOLD') or return $e->event;
4180 my $holds = $e->search_action_hold_request(
4183 fulfillment_time => undef,
4184 cancel_time => undef,
4188 my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
4189 $summary{_hold_status($e, $_)} += 1 for @$holds;
4195 __PACKAGE__->register_method(
4196 method => 'hold_has_copy_at',
4197 api_name => 'open-ils.circ.hold.has_copy_at',
4200 'Returns the ID of the found copy and name of the shelving location if there is ' .
4201 'an available copy at the specified org unit. Returns empty hash otherwise. ' .
4202 'The anticipated use for this method is to determine whether an item is ' .
4203 'available at the library where the user is placing the hold (or, alternatively, '.
4204 'at the pickup library) to encourage bypassing the hold placement and just ' .
4205 'checking out the item.' ,
4207 { desc => 'Authentication Token', type => 'string' },
4208 { desc => 'Method Arguments. Options include: hold_type, hold_target, org_unit. '
4209 . 'hold_type is the hold type code (T, V, C, M, ...). '
4210 . 'hold_target is the identifier of the hold target object. '
4211 . 'org_unit is org unit ID.',
4216 desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
4222 sub hold_has_copy_at {
4223 my($self, $conn, $auth, $args) = @_;
4225 my $e = new_editor(authtoken=>$auth);
4226 $e->checkauth or return $e->event;
4228 my $hold_type = $$args{hold_type};
4229 my $hold_target = $$args{hold_target};
4230 my $org_unit = $$args{org_unit};
4233 select => {acp => ['id'], acpl => ['name']},
4238 filter => { holdable => 't', deleted => 'f' },
4241 ccs => {field => 'id', filter => { holdable => 't'}, fkey => 'status' }
4244 where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit, status => [0,7]}},
4248 if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
4250 $query->{where}->{'+acp'}->{id} = $hold_target;
4252 } elsif($hold_type eq 'V') {
4254 $query->{where}->{'+acp'}->{call_number} = $hold_target;
4256 } elsif($hold_type eq 'P') {
4258 $query->{from}->{acp}->{acpm} = {
4259 field => 'target_copy',
4261 filter => {part => $hold_target},
4264 } elsif($hold_type eq 'I') {
4266 $query->{from}->{acp}->{sitem} = {
4269 filter => {issuance => $hold_target},
4272 } elsif($hold_type eq 'T') {
4274 $query->{from}->{acp}->{acn} = {
4276 fkey => 'call_number',
4280 filter => {id => $hold_target},
4288 $query->{from}->{acp}->{acn} = {
4290 fkey => 'call_number',
4299 filter => {metarecord => $hold_target},
4307 my $res = $e->json_query($query)->[0] or return {};
4308 return {copy => $res->{id}, location => $res->{name}} if $res;
4312 # returns true if the user already has an item checked out
4313 # that could be used to fulfill the requested hold.
4314 sub hold_item_is_checked_out {
4315 my($e, $user_id, $hold_type, $hold_target) = @_;
4318 select => {acp => ['id']},
4319 from => {acp => {}},
4323 in => { # copies for circs the user has checked out
4324 select => {circ => ['target_copy']},
4328 checkin_time => undef,
4330 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
4331 {stop_fines => undef}
4341 if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
4343 $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
4345 } elsif($hold_type eq 'V') {
4347 $query->{where}->{'+acp'}->{call_number} = $hold_target;
4349 } elsif($hold_type eq 'P') {
4351 $query->{from}->{acp}->{acpm} = {
4352 field => 'target_copy',
4354 filter => {part => $hold_target},
4357 } elsif($hold_type eq 'I') {
4359 $query->{from}->{acp}->{sitem} = {
4362 filter => {issuance => $hold_target},
4365 } elsif($hold_type eq 'T') {
4367 $query->{from}->{acp}->{acn} = {
4369 fkey => 'call_number',
4373 filter => {id => $hold_target},
4381 $query->{from}->{acp}->{acn} = {
4383 fkey => 'call_number',
4392 filter => {metarecord => $hold_target},
4400 return $e->json_query($query)->[0];
4403 __PACKAGE__->register_method(
4404 method => 'change_hold_title',
4405 api_name => 'open-ils.circ.hold.change_title',
4408 Updates all title level holds targeting the specified bibs to point a new bib./,
4410 { desc => 'Authentication Token', type => 'string' },
4411 { desc => 'New Target Bib Id', type => 'number' },
4412 { desc => 'Old Target Bib Ids', type => 'array' },
4414 return => { desc => '1 on success' }
4418 __PACKAGE__->register_method(
4419 method => 'change_hold_title_for_specific_holds',
4420 api_name => 'open-ils.circ.hold.change_title.specific_holds',
4423 Updates specified holds to target new bib./,
4425 { desc => 'Authentication Token', type => 'string' },
4426 { desc => 'New Target Bib Id', type => 'number' },
4427 { desc => 'Holds Ids for holds to update', type => 'array' },
4429 return => { desc => '1 on success' }
4434 sub change_hold_title {
4435 my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4437 my $e = new_editor(authtoken=>$auth, xact=>1);
4438 return $e->die_event unless $e->checkauth;
4440 my $holds = $e->search_action_hold_request(
4443 capture_time => undef,
4444 cancel_time => undef,
4445 fulfillment_time => undef,
4451 flesh_fields => { ahr => ['usr'] }
4457 for my $hold (@$holds) {
4458 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4459 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4460 $hold->target( $new_bib_id );
4461 $e->update_action_hold_request($hold) or return $e->die_event;
4466 _reset_hold($self, $e->requestor, $_) for @$holds;
4471 sub change_hold_title_for_specific_holds {
4472 my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4474 my $e = new_editor(authtoken=>$auth, xact=>1);
4475 return $e->die_event unless $e->checkauth;
4477 my $holds = $e->search_action_hold_request(
4480 capture_time => undef,
4481 cancel_time => undef,
4482 fulfillment_time => undef,
4488 flesh_fields => { ahr => ['usr'] }
4494 for my $hold (@$holds) {
4495 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4496 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4497 $hold->target( $new_bib_id );
4498 $e->update_action_hold_request($hold) or return $e->die_event;
4503 _reset_hold($self, $e->requestor, $_) for @$holds;
4508 __PACKAGE__->register_method(
4509 method => 'rec_hold_count',
4510 api_name => 'open-ils.circ.bre.holds.count',
4512 desc => q/Returns the total number of holds that target the
4513 selected bib record or its associated copies and call_numbers/,
4515 { desc => 'Bib ID', type => 'number' },
4516 { desc => q/Optional arguments. Supported arguments include:
4517 "pickup_lib_descendant" -> limit holds to those whose pickup
4518 library is equal to or is a child of the provided org unit/,
4522 return => {desc => 'Hold count', type => 'number'}
4526 __PACKAGE__->register_method(
4527 method => 'rec_hold_count',
4528 api_name => 'open-ils.circ.mmr.holds.count',
4530 desc => q/Returns the total number of holds that target the
4531 selected metarecord or its associated copies, call_numbers, and bib records/,
4533 { desc => 'Metarecord ID', type => 'number' },
4535 return => {desc => 'Hold count', type => 'number'}
4539 # XXX Need to add type I holds to these counts
4540 sub rec_hold_count {
4541 my($self, $conn, $target_id, $args) = @_;
4548 filter => {metarecord => $target_id}
4555 filter => { id => $target_id },
4560 if($self->api_name =~ /mmr/) {
4561 delete $bre_join->{bre}->{filter};
4562 $bre_join->{bre}->{join} = $mmr_join;
4568 fkey => 'call_number',
4574 select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4578 cancel_time => undef,
4579 fulfillment_time => undef,
4583 hold_type => [qw/C F R/],
4586 select => {acp => ['id']},
4587 from => { acp => $cn_join }
4597 select => {acn => ['id']},
4598 from => {acn => $bre_join}
4608 select => {bmp => ['id']},
4609 from => {bmp => $bre_join}
4617 target => $target_id
4625 if($self->api_name =~ /mmr/) {
4626 $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4631 select => {bre => ['id']},
4632 from => {bre => $mmr_join}
4638 $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4641 target => $target_id
4647 if (my $pld = $args->{pickup_lib_descendant}) {
4649 my $top_ou = new_editor()->search_actor_org_unit(
4650 {parent_ou => undef}
4651 )->[0]; # XXX Assumes single root node. Not alone in this...
4653 $query->{where}->{'+ahr'}->{pickup_lib} = {
4655 select => {aou => [{
4657 transform => 'actor.org_unit_descendants',
4658 result_field => 'id'
4661 where => {id => $pld}
4663 } if ($pld != $top_ou->id);
4666 # To avoid Internal Server Errors, we get an editor, then run the
4667 # query and check the result. If anything fails, we'll return 0.
4669 if (my $e = new_editor()) {
4670 my $query_result = $e->json_query($query);
4671 if ($query_result && @{$query_result}) {
4672 $result = $query_result->[0]->{count}
4679 # A helper function to calculate a hold's expiration time at a given
4680 # org_unit. Takes the org_unit as an argument and returns either the
4681 # hold expire time as an ISO8601 string or undef if there is no hold
4682 # expiration interval set for the subject ou.
4683 sub calculate_expire_time
4686 my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4688 my $date = DateTime->now->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($interval));
4689 return $U->epoch2ISO8601($date->epoch);
4695 __PACKAGE__->register_method(
4696 method => 'mr_hold_filter_attrs',
4697 api_name => 'open-ils.circ.mmr.holds.filters',
4702 Returns the set of available formats and languages for the
4703 constituent records of the provided metarcord.
4704 If an array of hold IDs is also provided, information about
4705 each is returned as well. This information includes:
4706 1. a slightly easier to read version of holdable_formats
4707 2. attributes describing the set of format icons included
4708 in the set of desired, constituent records.
4711 {desc => 'Metarecord ID', type => 'number'},
4712 {desc => 'Context Org ID', type => 'number'},
4713 {desc => 'Hold ID List', type => 'array'},
4717 Stream of objects. The first will have a 'metarecord' key
4718 containing non-hold-specific metarecord information, subsequent
4719 responses will contain a 'hold' key containing hold-specific
4727 sub mr_hold_filter_attrs {
4728 my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4729 my $e = new_editor();
4731 # by default, return MR / hold attributes for all constituent
4732 # records with holdable copies. If there is a hard boundary,
4733 # though, limit to records with copies within the boundary,
4734 # since anything outside the boundary can never be held.
4737 $org_depth = $U->ou_ancestor_setting_value(
4738 $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4741 # get all org-scoped records w/ holdable copies for this metarecord
4742 my ($bre_ids) = $self->method_lookup(
4743 'open-ils.circ.holds.metarecord.filtered_records')->run(
4744 $mr_id, undef, $org_id, $org_depth);
4746 my $item_lang_attr = 'item_lang'; # configurable?
4747 my $format_attr = $e->retrieve_config_global_flag(
4748 'opac.metarecord.holds.format_attr')->value;
4750 # helper sub for fetching ccvms for a batch of record IDs
4751 sub get_batch_ccvms {
4752 my ($e, $attr, $bre_ids) = @_;
4753 return [] unless $bre_ids and @$bre_ids;
4754 my $vals = $e->search_metabib_record_attr_flat({
4758 return [] unless @$vals;
4759 return $e->search_config_coded_value_map({
4761 code => [map {$_->value} @$vals]
4765 my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4766 my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4771 formats => $formats,
4776 return unless $hold_ids;
4777 my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4778 $icon_attr = $icon_attr ? $icon_attr->value : '';
4780 for my $hold_id (@$hold_ids) {
4781 my $hold = $e->retrieve_action_hold_request($hold_id)
4782 or return $e->event;
4784 next unless $hold->hold_type eq 'M';
4794 # collect the ccvm's for the selected formats / language
4795 # (i.e. the holdable formats) on the MR.
4796 # this assumes a two-key structure for format / language,
4797 # though no assumption is made about the keys themselves.
4798 my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4800 my $format_vals = [];
4801 for my $val (values %$hformats) {
4802 # val is either a single ccvm or an array of them
4803 $val = [$val] unless ref $val eq 'ARRAY';
4804 for my $node (@$val) {
4805 push (@$lang_vals, $node->{_val})
4806 if $node->{_attr} eq $item_lang_attr;
4807 push (@$format_vals, $node->{_val})
4808 if $node->{_attr} eq $format_attr;
4812 # fetch the ccvm's for consistency with the {metarecord} blob
4813 $resp->{hold}{formats} = $e->search_config_coded_value_map({
4814 ctype => $format_attr, code => $format_vals});
4815 $resp->{hold}{langs} = $e->search_config_coded_value_map({
4816 ctype => $item_lang_attr, code => $lang_vals});
4818 # find all of the bib records within this metarcord whose
4819 # format / language match the holdable formats on the hold
4820 my ($bre_ids) = $self->method_lookup(
4821 'open-ils.circ.holds.metarecord.filtered_records')->run(
4822 $hold->target, $hold->holdable_formats,
4823 $hold->selection_ou, $hold->selection_depth);
4825 # now find all of the 'icon' attributes for the records
4826 $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4827 $client->respond($resp);
4833 __PACKAGE__->register_method(
4834 method => "copy_has_holds_count",
4835 api_name => "open-ils.circ.copy.has_holds_count",
4839 Returns the number of holds a paticular copy has
4842 { desc => 'Authentication Token', type => 'string'},
4843 { desc => 'Copy ID', type => 'number'}
4854 sub copy_has_holds_count {
4855 my( $self, $conn, $auth, $copyid ) = @_;
4856 my $e = new_editor(authtoken=>$auth);
4857 return $e->event unless $e->checkauth;
4859 if( $copyid && $copyid > 0 ) {
4860 my $meth = 'retrieve_action_has_holds_count';
4861 my $data = $e->$meth($copyid);
4863 return $data->count();
4869 __PACKAGE__->register_method(
4870 method => "retrieve_holds_by_usr_notify_value_staff",
4871 api_name => "open-ils.circ.holds.retrieve_by_notify_staff",
4873 desc => "Retrieve the hold, for the specified user using the notify value. $ses_is_req_note",
4875 { desc => 'Authentication token', type => 'string' },
4876 { desc => 'User ID', type => 'number' },
4877 { desc => 'notify value', type => 'string' },
4878 { desc => 'notify_type', type => 'string' }
4881 desc => 'Hold objects with transits attached, event on error',
4886 sub retrieve_holds_by_usr_notify_value_staff {
4888 my($self, $conn, $auth, $usr_id, $contact, $cType) = @_;
4890 my $e = new_editor(authtoken=>$auth);
4891 $e->checkauth or return $e->event;
4893 if ($e->requestor->id != $usr_id){
4894 $e->allowed('VIEW_HOLD') or return $e->event;
4898 "select" => { "ahr" => ["id", "sms_notify", "phone_notify", "email_notify", "sms_carrier"]},
4902 "capture_time" => undef,
4903 "cancel_time" => undef,
4904 "fulfillment_time" => undef,
4908 if ($cType eq "day_phone" or $cType eq "evening_phone" or
4909 $cType eq "other_phone" or $cType eq "default_phone"){
4910 $q->{where}->{"-not"} = [
4911 { "phone_notify" => { "=" => $contact} },
4912 { "phone_notify" => { "<>" => undef } }
4917 if ($cType eq "default_sms") {
4918 $q->{where}->{"-not"} = [
4919 { "sms_notify" => { "=" => $contact} },
4920 { "sms_notify" => { "<>" => undef } }
4924 if ($cType eq "default_sms_carrier_id") {
4925 $q->{where}->{"-not"} = [
4926 { "sms_carrier" => { "=" => int($contact)} },
4927 { "sms_carrier" => { "<>" => undef } }
4931 if ($cType =~ /notify/){
4932 # this is was notification pref change
4933 # we find all unfulfilled holds that match have that pref
4934 my $optr = $contact == 1 ? "<>" : "="; # unless it's email, true val means we want to query for not null
4935 my $conj = $optr eq '=' ? '-or' : '-and';
4936 if ($cType =~ /sms/) {
4937 $q->{where}->{$conj} = [ { sms_notify => { $optr => undef } }, { sms_notify => { $optr => '' } } ];
4939 if ($cType =~ /phone/) {
4940 $q->{where}->{$conj} = [ { phone_notify => { $optr => undef } }, { phone_notify => { $optr => '' } } ];
4942 if ($cType =~ /email/) {
4944 $q->{where}->{'+ahr'} = 'email_notify';
4946 $q->{where}->{'-not'} = {'+ahr' => 'email_notify'};
4951 my $holds = $e->json_query($q);
4952 #$hold_ids = [ map { $_->{id} } @$hold_ids ];
4957 __PACKAGE__->register_method(
4958 method => "batch_update_holds_by_value_staff",
4959 api_name => "open-ils.circ.holds.batch_update_holds_by_notify_staff",
4961 desc => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
4963 { desc => 'Authentication token', type => 'string' },
4964 { desc => 'User ID', type => 'number' },
4965 { desc => 'Hold IDs', type => 'array' },
4966 { desc => 'old notify value', type => 'string' },
4967 { desc => 'new notify value', type => 'string' },
4968 { desc => 'field name', type => 'string' },
4969 { desc => 'SMS carrier ID', type => 'number' }
4973 desc => 'Hold objects with transits attached, event on error',
4978 sub batch_update_holds_by_value_staff {
4979 my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType, $carrierId) = @_;
4981 my $e = new_editor(authtoken=>$auth, xact=>1);
4982 $e->checkauth or return $e->event;
4983 if ($e->requestor->id != $usr_id){
4984 $e->allowed('UPDATE_HOLD') or return $e->event;
4988 for my $id (@$hold_ids) {
4990 my $hold = $e->retrieve_action_hold_request($id);
4992 if ($cType eq "day_phone" or $cType eq "evening_phone" or
4993 $cType eq "other_phone" or $cType eq "default_phone") {
4995 if ($newval eq '') {
4996 $hold->clear_phone_notify();
4999 $hold->phone_notify($newval);
5003 if ($cType eq "default_sms"){
5004 if ($newval eq '') {
5005 $hold->clear_sms_notify();
5006 $hold->clear_sms_carrier(); # TODO: prevent orphan sms_carrier, via db trigger
5009 $hold->sms_notify($newval);
5010 $hold->sms_carrier($carrierId);
5015 if ($cType eq "default_sms_carrier_id") {
5016 $hold->sms_carrier($newval);
5019 if ($cType =~ /notify/){
5020 # this is a notification pref change
5021 if ($cType =~ /email/) { $hold->email_notify($newval); }
5022 if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
5023 if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
5024 # the other case, where x_notify is changed to true,
5025 # is covered by an actual value being assigned
5028 $e->update_action_hold_request($hold) or return $e->die_event;
5033 $e->commit; #unless $U->event_code($res);
5039 __PACKAGE__->register_method(
5040 method => "retrieve_holds_by_usr_with_notify",
5041 api_name => "open-ils.circ.holds.retrieve.by_usr.with_notify",
5043 desc => "Retrieve the hold, for the specified user using the notify value. $ses_is_req_note",
5045 { desc => 'Authentication token', type => 'string' },
5046 { desc => 'User ID', type => 'number' },
5049 desc => 'Lists of holds with notification values, event on error',
5054 sub retrieve_holds_by_usr_with_notify {
5056 my($self, $conn, $auth, $usr_id) = @_;
5058 my $e = new_editor(authtoken=>$auth);
5059 $e->checkauth or return $e->event;
5061 if ($e->requestor->id != $usr_id){
5062 $e->allowed('VIEW_HOLD') or return $e->event;
5066 "select" => { "ahr" => ["id", "phone_notify", "email_notify", "sms_carrier", "sms_notify"]},
5070 "capture_time" => undef,
5071 "cancel_time" => undef,
5072 "fulfillment_time" => undef,
5076 my $holds = $e->json_query($q);
5080 __PACKAGE__->register_method(
5081 method => "batch_update_holds_by_value",
5082 api_name => "open-ils.circ.holds.batch_update_holds_by_notify",
5084 desc => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
5086 { desc => 'Authentication token', type => 'string' },
5087 { desc => 'User ID', type => 'number' },
5088 { desc => 'Hold IDs', type => 'array' },
5089 { desc => 'old notify value', type => 'string' },
5090 { desc => 'new notify value', type => 'string' },
5091 { desc => 'notify_type', type => 'string' }
5094 desc => 'Hold objects with transits attached, event on error',
5099 sub batch_update_holds_by_value {
5100 my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType) = @_;
5102 my $e = new_editor(authtoken=>$auth, xact=>1);
5103 $e->checkauth or return $e->event;
5104 if ($e->requestor->id != $usr_id){
5105 $e->allowed('UPDATE_HOLD') or return $e->event;
5109 for my $id (@$hold_ids) {
5111 my $hold = $e->retrieve_action_hold_request(int($id));
5113 if ($cType eq "day_phone" or $cType eq "evening_phone" or
5114 $cType eq "other_phone" or $cType eq "default_phone") {
5115 # change phone number value on hold
5116 $hold->phone_notify($newval);
5118 if ($cType eq "default_sms") {
5119 # change SMS number value on hold
5120 $hold->sms_notify($newval);
5123 if ($cType eq "default_sms_carrier_id") {
5124 $hold->sms_carrier(int($newval));
5127 if ($cType =~ /notify/){
5128 # this is a notification pref change
5129 if ($cType =~ /email/) { $hold->email_notify($newval); }
5130 if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
5131 if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
5132 # the other case, where x_notify is changed to true,
5133 # is covered by an actual value being assigned
5136 $e->update_action_hold_request($hold) or return $e->die_event;
5141 $e->commit; #unless $U->event_code($res);
5145 __PACKAGE__->register_method(
5146 method => "hold_metadata",
5147 api_name => "open-ils.circ.hold.get_metadata",
5152 Returns a stream of objects containing whatever bib,
5153 volume, etc. data is available to the specific hold
5157 {desc => 'Hold Type', type => 'string'},
5158 {desc => 'Hold Target(s)', type => 'number or array'},
5159 {desc => 'Context org unit (optional)', type => 'number'}
5163 Stream of hold metadata objects.
5172 my ($self, $client, $hold_type, $hold_targets, $org_id) = @_;
5174 $hold_targets = [$hold_targets] unless ref $hold_targets;
5176 my $e = new_editor();
5177 for my $target (@$hold_targets) {
5179 # create a dummy hold for find_hold_mvr
5180 my $hold = Fieldmapper::action::hold_request->new;
5181 $hold->hold_type($hold_type);
5182 $hold->target($target);
5184 my (undef, $volume, $copy, $issuance, $part, $bre, $metarecord) =
5185 find_hold_mvr($e, $hold, {suppress_mvr => 1});
5187 $bre->clear_marc; # avoid bulk
5193 issuance => $issuance,
5196 part_required => 'f',
5198 metarecord => $metarecord,
5199 metarecord_filters => {}
5202 # If this is a bib hold or metarecord hold, also return the
5203 # available set of MR filters (AKA "Holdable Formats") for the
5204 # hold. For bib holds these may be used to upgrade the hold
5205 # from a bib to metarecord hold.
5206 if ($hold_type eq 'T') {
5207 my $map = $e->search_metabib_metarecord_source_map(
5208 {source => $meta->{bibrecord}->id})->[0];
5211 $meta->{metarecord} =
5212 $e->retrieve_metabib_metarecord($map->metarecord);
5215 # Also fetch the available parts for bib-level holds.
5216 $meta->{parts} = $e->search_biblio_monograph_part(
5218 {record => $bre->id, deleted => 'f'},
5219 {order_by => {bmp => 'label_sortkey'}}
5223 # T holds on records that have parts are normally OK, but if the record has
5224 # no non-part copies, the hold will ultimately fail. When that happens,
5225 # require the user to select a part.
5227 # If the global flag circ.holds.api_require_monographic_part_when_present is
5228 # enabled, or the library setting circ.holds.ui_require_monographic_part_when_present
5229 # is true for any involved owning_library, then also require part selection.
5230 my $part_required = 0;
5231 if ($meta->{parts}) {
5232 my $part_required_flag = $e->retrieve_config_global_flag('circ.holds.api_require_monographic_part_when_present');
5233 $part_required = ($part_required_flag and $U->is_true($part_required_flag->enabled));
5234 if (!$part_required) {
5235 my $resp = $e->json_query({
5237 acn => ['owning_lib']
5239 from => {acn => {acp => {type => 'left'}}},
5244 {id => undef} # left join
5247 '+acn' => {deleted => 'f', record => $bre->id}
5251 my $org_ids = [map {$_->{owning_lib}} @$resp];
5252 foreach my $org (@$org_ids) { # FIXME: worth shortcutting/optimizing?
5253 if ($U->ou_ancestor_setting_value($org, 'circ.holds.ui_require_monographic_part_when_present')) {
5258 if (!$part_required) {
5259 my $np_copies = $e->json_query({
5260 select => { acp => [{column => 'id', transform => 'count', alias => 'count'}]},
5261 from => {acp => {acn => {}, acpm => {type => 'left'}}},
5263 '+acp' => {deleted => 'f'},
5264 '+acn' => {deleted => 'f', record => $bre->id},
5265 '+acpm' => {id => undef}
5268 $part_required = 1 if $np_copies->[0]->{count} == 0;
5271 $meta->{part_required} = $part_required;
5274 if ($meta->{metarecord}) {
5277 $self->method_lookup('open-ils.circ.mmr.holds.filters')
5278 ->run($meta->{metarecord}->id, $org_id);
5281 $meta->{metarecord_filters} = $filters->{metarecord};
5285 $client->respond($meta);