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}
2043 "fkey" => "call_number",
2057 "fkey" => "circ_lib",
2060 "location" => {"=" => {"+acp" => "location"}}
2068 "is_available" => "t"
2077 "capture_time" => undef,
2078 "cancel_time" => undef,
2080 {"expire_time" => undef },
2081 {"expire_time" => {">" => "now"}}
2085 (@$sort ? (order_by => $sort) : ()),
2086 ($$params{limit} ? (limit => $$params{limit}) : ()),
2087 ($$params{offset} ? (offset => $$params{offset}) : ())
2088 }, {"substream" => 1}
2089 ) or return $e->die_event;
2091 $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
2094 for my $hid (@$holds_ids) {
2095 push @chunk, $e->retrieve_action_hold_request([
2099 "ahr" => ["usr", "current_copy"],
2101 "acp" => ["location", "call_number", "parts"],
2102 "acn" => ["record","prefix","suffix"]
2107 if (@chunk >= $$params{chunk_size}) {
2108 $client->respond( \@chunk );
2112 $client->respond_complete( \@chunk ) if (@chunk);
2119 __PACKAGE__->register_method(
2120 method => 'fetch_hold_notify',
2121 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
2124 Returns a list of hold notification objects based on hold id.
2125 @param authtoken The loggin session key
2126 @param holdid The id of the hold whose notifications we want to retrieve
2127 @return An array of hold notification objects, event on error.
2131 sub fetch_hold_notify {
2132 my( $self, $conn, $authtoken, $holdid ) = @_;
2133 my( $requestor, $evt ) = $U->checkses($authtoken);
2134 return $evt if $evt;
2135 my ($hold, $patron);
2136 ($hold, $evt) = $U->fetch_hold($holdid);
2137 return $evt if $evt;
2138 ($patron, $evt) = $U->fetch_user($hold->usr);
2139 return $evt if $evt;
2141 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
2142 return $evt if $evt;
2144 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
2145 return $U->cstorereq(
2146 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
2150 __PACKAGE__->register_method(
2151 method => 'create_hold_notify',
2152 api_name => 'open-ils.circ.hold_notification.create',
2154 Creates a new hold notification object
2155 @param authtoken The login session key
2156 @param notification The hold notification object to create
2157 @return ID of the new object on success, Event on error
2161 sub create_hold_notify {
2162 my( $self, $conn, $auth, $note ) = @_;
2163 my $e = new_editor(authtoken=>$auth, xact=>1);
2164 return $e->die_event unless $e->checkauth;
2166 my $hold = $e->retrieve_action_hold_request($note->hold)
2167 or return $e->die_event;
2168 my $patron = $e->retrieve_actor_user($hold->usr)
2169 or return $e->die_event;
2171 return $e->die_event unless
2172 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
2174 $note->notify_staff($e->requestor->id);
2175 $e->create_action_hold_notification($note) or return $e->die_event;
2180 __PACKAGE__->register_method(
2181 method => 'create_hold_note',
2182 api_name => 'open-ils.circ.hold_note.create',
2184 Creates a new hold request note object
2185 @param authtoken The login session key
2186 @param note The hold note object to create
2187 @return ID of the new object on success, Event on error
2191 sub create_hold_note {
2192 my( $self, $conn, $auth, $note ) = @_;
2193 my $e = new_editor(authtoken=>$auth, xact=>1);
2194 return $e->die_event unless $e->checkauth;
2196 my $hold = $e->retrieve_action_hold_request($note->hold)
2197 or return $e->die_event;
2198 my $patron = $e->retrieve_actor_user($hold->usr)
2199 or return $e->die_event;
2201 return $e->die_event unless
2202 $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
2204 $e->create_action_hold_request_note($note) or return $e->die_event;
2209 __PACKAGE__->register_method(
2210 method => 'reset_hold',
2211 api_name => 'open-ils.circ.hold.reset',
2213 Un-captures and un-targets a hold, essentially returning
2214 it to the state it was in directly after it was placed,
2215 then attempts to re-target the hold
2216 @param authtoken The login session key
2217 @param holdid The id of the hold
2223 my( $self, $conn, $auth, $holdid ) = @_;
2225 my ($hold, $evt) = $U->fetch_hold($holdid);
2226 return $evt if $evt;
2227 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
2228 return $evt if $evt;
2229 $evt = _reset_hold($self, $reqr, $hold);
2230 return $evt if $evt;
2235 __PACKAGE__->register_method(
2236 method => 'reset_hold_batch',
2237 api_name => 'open-ils.circ.hold.reset.batch'
2240 sub reset_hold_batch {
2241 my($self, $conn, $auth, $hold_ids) = @_;
2243 my $e = new_editor(authtoken => $auth);
2244 return $e->event unless $e->checkauth;
2246 for my $hold_id ($hold_ids) {
2248 my $hold = $e->retrieve_action_hold_request(
2249 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
2250 or return $e->event;
2252 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
2253 _reset_hold($self, $e->requestor, $hold);
2261 my ($self, $reqr, $hold) = @_;
2263 my $e = new_editor(xact =>1, requestor => $reqr);
2265 $logger->info("reseting hold ".$hold->id);
2267 my $hid = $hold->id;
2269 if( $hold->capture_time and $hold->current_copy ) {
2271 my $copy = $e->retrieve_asset_copy($hold->current_copy)
2272 or return $e->die_event;
2274 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2275 $logger->info("setting copy to status 'reshelving' on hold retarget");
2276 $copy->status(OILS_COPY_STATUS_RESHELVING);
2277 $copy->editor($e->requestor->id);
2278 $copy->edit_date('now');
2279 $e->update_asset_copy($copy) or return $e->die_event;
2281 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
2283 $logger->warn("! reseting hold [$hid] that is in transit");
2284 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
2287 my $trans = $e->retrieve_action_transit_copy($transid);
2289 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
2290 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
2291 $logger->info("Transit abort completed with result $evt");
2292 unless ("$evt" eq 1) {
2301 $hold->clear_capture_time;
2302 $hold->clear_current_copy;
2303 $hold->clear_shelf_time;
2304 $hold->clear_shelf_expire_time;
2305 $hold->clear_current_shelf_lib;
2307 $e->update_action_hold_request($hold) or return $e->die_event;
2310 $U->simplereq('open-ils.hold-targeter',
2311 'open-ils.hold-targeter.target', {hold => $hold->id});
2317 __PACKAGE__->register_method(
2318 method => 'fetch_open_title_holds',
2319 api_name => 'open-ils.circ.open_holds.retrieve',
2321 Returns a list ids of un-fulfilled holds for a given title id
2322 @param authtoken The login session key
2323 @param id the id of the item whose holds we want to retrieve
2324 @param type The hold type - M, T, I, V, C, F, R
2328 sub fetch_open_title_holds {
2329 my( $self, $conn, $auth, $id, $type, $org ) = @_;
2330 my $e = new_editor( authtoken => $auth );
2331 return $e->event unless $e->checkauth;
2334 $org ||= $e->requestor->ws_ou;
2336 # return $e->search_action_hold_request(
2337 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
2339 # XXX make me return IDs in the future ^--
2340 my $holds = $e->search_action_hold_request(
2343 cancel_time => undef,
2345 fulfillment_time => undef
2349 flesh_hold_transits($holds);
2354 sub flesh_hold_transits {
2356 for my $hold ( @$holds ) {
2358 $apputils->simplereq(
2360 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
2361 { hold => $hold->id, cancel_time => undef },
2362 { order_by => { ahtc => 'id desc' }, limit => 1 }
2368 sub flesh_hold_notices {
2369 my( $holds, $e ) = @_;
2370 $e ||= new_editor();
2372 for my $hold (@$holds) {
2373 my $notices = $e->search_action_hold_notification(
2375 { hold => $hold->id },
2376 { order_by => { anh => 'notify_time desc' } },
2381 $hold->notify_count(scalar(@$notices));
2383 my $n = $e->retrieve_action_hold_notification($$notices[0])
2384 or return $e->event;
2385 $hold->notify_time($n->notify_time);
2391 __PACKAGE__->register_method(
2392 method => 'fetch_captured_holds',
2393 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
2397 Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
2398 @param authtoken The login session key
2399 @param org The org id of the location in question
2400 @param match_copy A specific copy to limit to
2404 __PACKAGE__->register_method(
2405 method => 'fetch_captured_holds',
2406 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
2410 Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
2411 @param authtoken The login session key
2412 @param org The org id of the location in question
2413 @param match_copy A specific copy to limit to
2417 __PACKAGE__->register_method(
2418 method => 'fetch_captured_holds',
2419 api_name => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
2423 Returns list ids of shelf-expired un-fulfilled holds for a given title id
2424 @param authtoken The login session key
2425 @param org The org id of the location in question
2426 @param match_copy A specific copy to limit to
2430 __PACKAGE__->register_method(
2431 method => 'fetch_captured_holds',
2433 'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
2437 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2438 for a given shelf lib
2442 __PACKAGE__->register_method(
2443 method => 'fetch_captured_holds',
2445 'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
2449 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2450 for a given shelf lib
2455 sub fetch_captured_holds {
2456 my( $self, $conn, $auth, $org, $match_copy ) = @_;
2458 my $e = new_editor(authtoken => $auth);
2459 return $e->die_event unless $e->checkauth;
2460 return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
2462 $org ||= $e->requestor->ws_ou;
2464 my $current_copy = { '!=' => undef };
2465 $current_copy = { '=' => $match_copy } if $match_copy;
2468 select => { alhr => ['id'] },
2473 fkey => 'current_copy'
2478 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
2480 capture_time => { "!=" => undef },
2481 current_copy => $current_copy,
2482 fulfillment_time => undef,
2483 current_shelf_lib => $org
2487 if($self->api_name =~ /expired/) {
2488 $query->{'where'}->{'+alhr'}->{'-or'} = {
2489 shelf_expire_time => { '<' => 'today'},
2490 cancel_time => { '!=' => undef },
2493 my $hold_ids = $e->json_query( $query );
2495 if ($self->api_name =~ /wrong_shelf/) {
2496 # fetch holds whose current_shelf_lib is $org, but whose pickup
2497 # lib is some other org unit. Ignore already-retrieved holds.
2499 pickup_lib_changed_on_shelf_holds(
2500 $e, $org, [map {$_->{id}} @$hold_ids]);
2501 # match the layout of other items in $hold_ids
2502 push (@$hold_ids, {id => $_}) for @$wrong_shelf;
2506 for my $hold_id (@$hold_ids) {
2507 if($self->api_name =~ /id_list/) {
2508 $conn->respond($hold_id->{id});
2512 $e->retrieve_action_hold_request([
2516 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
2517 order_by => {anh => 'notify_time desc'}
2527 __PACKAGE__->register_method(
2528 method => "print_expired_holds_stream",
2529 api_name => "open-ils.circ.captured_holds.expired.print.stream",
2533 sub print_expired_holds_stream {
2534 my ($self, $client, $auth, $params) = @_;
2536 # No need to check specific permissions: we're going to call another method
2537 # that will do that.
2538 my $e = new_editor("authtoken" => $auth);
2539 return $e->die_event unless $e->checkauth;
2541 delete($$params{org_id}) unless (int($$params{org_id}));
2542 delete($$params{limit}) unless (int($$params{limit}));
2543 delete($$params{offset}) unless (int($$params{offset}));
2544 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2545 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2546 $$params{chunk_size} ||= 10;
2547 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2549 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2551 my @hold_ids = $self->method_lookup(
2552 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
2553 )->run($auth, $params->{"org_id"});
2558 } elsif (defined $U->event_code($hold_ids[0])) {
2560 return $hold_ids[0];
2563 $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
2566 my @hid_chunk = splice @hold_ids, 0, $params->{"chunk_size"};
2568 my $result_chunk = $e->json_query({
2570 "acp" => ["barcode"],
2572 first_given_name second_given_name family_name alias
2581 "field" => "id", "fkey" => "current_copy",
2584 "field" => "id", "fkey" => "call_number",
2587 "field" => "id", "fkey" => "record"
2591 "acpl" => {"field" => "id", "fkey" => "location"}
2594 "au" => {"field" => "id", "fkey" => "usr"}
2597 "where" => {"+ahr" => {"id" => \@hid_chunk}}
2598 }) or return $e->die_event;
2599 $client->respond($result_chunk);
2606 __PACKAGE__->register_method(
2607 method => "check_title_hold_batch",
2608 api_name => "open-ils.circ.title_hold.is_possible.batch",
2611 desc => '@see open-ils.circ.title_hold.is_possible.batch',
2613 { desc => 'Authentication token', type => 'string'},
2614 { desc => 'Array of Hash of named parameters', type => 'array'},
2617 desc => 'Array of response objects',
2623 sub check_title_hold_batch {
2624 my($self, $client, $authtoken, $param_list, $oargs) = @_;
2625 foreach (@$param_list) {
2626 my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
2627 $client->respond($res);
2633 __PACKAGE__->register_method(
2634 method => "check_title_hold",
2635 api_name => "open-ils.circ.title_hold.is_possible",
2637 desc => 'Determines if a hold were to be placed by a given user, ' .
2638 'whether or not said hold would have any potential copies to fulfill it.' .
2639 'The named paramaters of the second argument include: ' .
2640 'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
2641 'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
2643 { desc => 'Authentication token', type => 'string'},
2644 { desc => 'Hash of named parameters', type => 'object'},
2647 desc => 'List of new message IDs (empty if none)',
2653 =head3 check_title_hold (token, hash)
2655 The named fields in the hash are:
2657 patronid - ID of the hold recipient (required)
2658 depth - hold range depth (default 0)
2659 pickup_lib - destination for hold, fallback value for selection_ou
2660 selection_ou - ID of org_unit establishing hard and soft hold boundary settings
2661 issuanceid - ID of the issuance to be held, required for Issuance level hold
2662 partid - ID of the monograph part to be held, required for monograph part level hold
2663 titleid - ID (BRN) of the title to be held, required for Title level hold
2664 volume_id - required for Volume level hold
2665 copy_id - required for Copy level hold
2666 mrid - required for Meta-record level hold
2667 hold_type - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record (default "T")
2669 All key/value pairs are passed on to do_possibility_checks.
2673 # FIXME: better params checking. what other params are required, if any?
2674 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
2675 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
2676 # used in conditionals, where it may be undefined, causing a warning.
2677 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
2679 sub check_title_hold {
2680 my( $self, $client, $authtoken, $params ) = @_;
2681 my $e = new_editor(authtoken=>$authtoken);
2682 return $e->event unless $e->checkauth;
2684 my %params = %$params;
2685 my $depth = $params{depth} || 0;
2686 $params{depth} = $depth; #define $params{depth} if unset, since it gets used later
2687 my $selection_ou = $params{selection_ou} || $params{pickup_lib};
2688 my $oargs = $params{oargs} || {};
2690 if($oargs->{events}) {
2691 @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
2695 my $patron = $e->retrieve_actor_user($params{patronid})
2696 or return $e->event;
2698 if( $e->requestor->id ne $patron->id ) {
2699 return $e->event unless
2700 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
2703 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
2705 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
2706 or return $e->event;
2708 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
2709 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
2712 my $return_depth = $hard_boundary; # default depth to return on success
2713 if(defined $soft_boundary and $depth < $soft_boundary) {
2714 # work up the tree and as soon as we find a potential copy, use that depth
2715 # also, make sure we don't go past the hard boundary if it exists
2717 # our min boundary is the greater of user-specified boundary or hard boundary
2718 my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
2719 $hard_boundary : $depth;
2721 my $depth = $soft_boundary;
2722 while($depth >= $min_depth) {
2723 $logger->info("performing hold possibility check with soft boundary $depth");
2724 @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
2726 $return_depth = $depth;
2731 } elsif(defined $hard_boundary and $depth < $hard_boundary) {
2732 # there is no soft boundary, enforce the hard boundary if it exists
2733 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
2734 @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
2736 # no boundaries defined, fall back to user specifed boundary or no boundary
2737 $logger->info("performing hold possibility check with no boundary");
2738 @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
2741 my $place_unfillable = 0;
2742 $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
2747 "depth" => $return_depth,
2748 "local_avail" => $status[1]
2750 } elsif ($status[2]) {
2751 my $n = scalar @{$status[2]};
2752 return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2754 return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2760 sub do_possibility_checks {
2761 my($e, $patron, $request_lib, $depth, %params) = @_;
2763 my $issuanceid = $params{issuanceid} || "";
2764 my $partid = $params{partid} || "";
2765 my $titleid = $params{titleid} || "";
2766 my $volid = $params{volume_id};
2767 my $copyid = $params{copy_id};
2768 my $mrid = $params{mrid} || "";
2769 my $pickup_lib = $params{pickup_lib};
2770 my $hold_type = $params{hold_type} || 'T';
2771 my $selection_ou = $params{selection_ou} || $pickup_lib;
2772 my $holdable_formats = $params{holdable_formats};
2773 my $oargs = $params{oargs} || {};
2780 if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
2782 return $e->event unless $copy = $e->retrieve_asset_copy($copyid);
2783 return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
2784 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2786 return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
2787 return verify_copy_for_hold(
2788 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
2791 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2793 return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
2794 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2796 return _check_volume_hold_is_possible(
2797 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2800 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
2802 return _check_title_hold_is_possible(
2803 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
2806 } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
2808 return _check_issuance_hold_is_possible(
2809 $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2812 } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
2814 return _check_monopart_hold_is_possible(
2815 $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2818 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2820 # pasing undef as the depth to filtered_records causes the depth
2821 # of the selection_ou to be used, which is not what we want here.
2824 my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
2826 for my $rec (@$recs) {
2827 @status = _check_title_hold_is_possible(
2828 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
2834 # else { Unrecognized hold_type ! } # FIXME: return error? or 0?
2837 sub MR_filter_records {
2844 my $opac_visible = shift;
2846 my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
2847 return $U->storagereq(
2848 'open-ils.storage.metarecord.filtered_records.atomic',
2849 $m, $f, $org_at_depth, $opac_visible
2852 __PACKAGE__->register_method(
2853 method => 'MR_filter_records',
2854 api_name => 'open-ils.circ.holds.metarecord.filtered_records',
2859 sub create_ranged_org_filter {
2860 my($e, $selection_ou, $depth) = @_;
2862 # find the orgs from which this hold may be fulfilled,
2863 # based on the selection_ou and depth
2865 my $top_org = $e->search_actor_org_unit([
2866 {parent_ou => undef},
2867 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
2870 return () if $depth == $top_org->ou_type->depth;
2872 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
2873 %org_filter = (circ_lib => []);
2874 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
2876 $logger->info("hold org filter at depth $depth and selection_ou ".
2877 "$selection_ou created list of @{$org_filter{circ_lib}}");
2883 sub _check_title_hold_is_possible {
2884 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
2885 # $holdable_formats is now unused. We pre-filter the MR's records.
2887 my $e = new_editor();
2889 # T holds on records that have parts are normally OK, but if the record has
2890 # no non-part copies, the hold will ultimately fail, so let's test for that.
2892 # If the global flag circ.holds.api_require_monographic_part_when_present is
2893 # enabled, then any configured parts for the bib is enough to disallow title holds.
2894 my $part_required = 0;
2895 my $parts = $e->search_biblio_monograph_part(
2901 my $part_required_flag = $e->retrieve_config_global_flag('circ.holds.api_require_monographic_part_when_present');
2902 $part_required = ($part_required_flag and $U->is_true($part_required_flag->enabled));
2903 if (!$part_required) {
2904 my $np_copies = $e->json_query({
2905 select => { acp => [{column => 'id', transform => 'count', alias => 'count'}]},
2906 from => {acp => {acn => {}, acpm => {type => 'left'}}},
2908 '+acp' => {deleted => 'f'},
2909 '+acn' => {deleted => 'f', record => $titleid},
2910 '+acpm' => {id => undef}
2913 $part_required = 1 if $np_copies->[0]->{count} == 0;
2916 if ($part_required) {
2917 $logger->info("title hold when monographic part required");
2921 "TITLE_HOLD_WHEN_MONOGRAPHIC_PART_REQUIRED",
2922 "payload" => {"fail_part" => "monographic_part_required"}
2928 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2930 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2931 my $copies = $e->json_query(
2933 select => { acp => ['id', 'circ_lib'] },
2938 fkey => 'call_number',
2939 filter => { record => $titleid }
2943 filter => { holdable => 't', deleted => 'f' },
2946 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' },
2947 acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2951 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2952 '+acpm' => { target_copy => undef } # ignore part-linked copies
2957 $logger->info("title possible found ".scalar(@$copies)." potential copies");
2961 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2962 "payload" => {"fail_part" => "no_ultimate_items"}
2967 # -----------------------------------------------------------------------
2968 # sort the copies into buckets based on their circ_lib proximity to
2969 # the patron's home_ou.
2970 # -----------------------------------------------------------------------
2972 my $home_org = $patron->home_ou;
2973 my $req_org = $request_lib->id;
2975 $prox_cache{$home_org} =
2976 $e->search_actor_org_unit_proximity({from_org => $home_org})
2977 unless $prox_cache{$home_org};
2978 my $home_prox = $prox_cache{$home_org};
2979 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2982 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2983 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2985 my @keys = sort { $a <=> $b } keys %buckets;
2988 if( $home_org ne $req_org ) {
2989 # -----------------------------------------------------------------------
2990 # shove the copies close to the request_lib into the primary buckets
2991 # directly before the farthest away copies. That way, they are not
2992 # given priority, but they are checked before the farthest copies.
2993 # -----------------------------------------------------------------------
2994 $prox_cache{$req_org} =
2995 $e->search_actor_org_unit_proximity({from_org => $req_org})
2996 unless $prox_cache{$req_org};
2997 my $req_prox = $prox_cache{$req_org};
3000 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3001 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3003 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
3004 my $new_key = $highest_key - 0.5; # right before the farthest prox
3005 my @keys2 = sort { $a <=> $b } keys %buckets2;
3006 for my $key (@keys2) {
3007 last if $key >= $highest_key;
3008 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3012 @keys = sort { $a <=> $b } keys %buckets;
3017 my $age_protect_only = 0;
3018 OUTER: for my $key (@keys) {
3019 my @cps = @{$buckets{$key}};
3021 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3023 for my $copyid (@cps) {
3025 next if $seen{$copyid};
3026 $seen{$copyid} = 1; # there could be dupes given the merged buckets
3027 my $copy = $e->retrieve_asset_copy($copyid);
3028 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3030 unless($title) { # grab the title if we don't already have it
3031 my $vol = $e->retrieve_asset_call_number(
3032 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3033 $title = $vol->record;
3036 @status = verify_copy_for_hold(
3037 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3039 $age_protect_only ||= $status[3];
3040 last OUTER if $status[0];
3044 $status[3] = $age_protect_only;
3048 sub _check_issuance_hold_is_possible {
3049 my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3051 my $e = new_editor();
3052 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
3054 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
3055 my $copies = $e->json_query(
3057 select => { acp => ['id', 'circ_lib'] },
3063 filter => { issuance => $issuanceid }
3067 filter => { holdable => 't', deleted => 'f' },
3070 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3074 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3080 $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
3084 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3085 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3090 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3091 "payload" => {"fail_part" => "no_ultimate_items"}
3099 # -----------------------------------------------------------------------
3100 # sort the copies into buckets based on their circ_lib proximity to
3101 # the patron's home_ou.
3102 # -----------------------------------------------------------------------
3104 my $home_org = $patron->home_ou;
3105 my $req_org = $request_lib->id;
3107 $prox_cache{$home_org} =
3108 $e->search_actor_org_unit_proximity({from_org => $home_org})
3109 unless $prox_cache{$home_org};
3110 my $home_prox = $prox_cache{$home_org};
3111 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3114 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3115 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3117 my @keys = sort { $a <=> $b } keys %buckets;
3120 if( $home_org ne $req_org ) {
3121 # -----------------------------------------------------------------------
3122 # shove the copies close to the request_lib into the primary buckets
3123 # directly before the farthest away copies. That way, they are not
3124 # given priority, but they are checked before the farthest copies.
3125 # -----------------------------------------------------------------------
3126 $prox_cache{$req_org} =
3127 $e->search_actor_org_unit_proximity({from_org => $req_org})
3128 unless $prox_cache{$req_org};
3129 my $req_prox = $prox_cache{$req_org};
3132 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3133 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3135 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
3136 my $new_key = $highest_key - 0.5; # right before the farthest prox
3137 my @keys2 = sort { $a <=> $b } keys %buckets2;
3138 for my $key (@keys2) {
3139 last if $key >= $highest_key;
3140 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3144 @keys = sort { $a <=> $b } keys %buckets;
3149 my $age_protect_only = 0;
3150 OUTER: for my $key (@keys) {
3151 my @cps = @{$buckets{$key}};
3153 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3155 for my $copyid (@cps) {
3157 next if $seen{$copyid};
3158 $seen{$copyid} = 1; # there could be dupes given the merged buckets
3159 my $copy = $e->retrieve_asset_copy($copyid);
3160 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3162 unless($title) { # grab the title if we don't already have it
3163 my $vol = $e->retrieve_asset_call_number(
3164 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3165 $title = $vol->record;
3168 @status = verify_copy_for_hold(
3169 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3171 $age_protect_only ||= $status[3];
3172 last OUTER if $status[0];
3177 if (!defined($empty_ok)) {
3178 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3179 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3182 return (1,0) if ($empty_ok);
3184 $status[3] = $age_protect_only;
3188 sub _check_monopart_hold_is_possible {
3189 my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3191 my $e = new_editor();
3192 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
3194 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
3195 my $copies = $e->json_query(
3197 select => { acp => ['id', 'circ_lib'] },
3201 field => 'target_copy',
3203 filter => { part => $partid }
3207 filter => { holdable => 't', deleted => 'f' },
3210 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3214 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3220 $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
3224 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3225 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3230 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3231 "payload" => {"fail_part" => "no_ultimate_items"}
3239 # -----------------------------------------------------------------------
3240 # sort the copies into buckets based on their circ_lib proximity to
3241 # the patron's home_ou.
3242 # -----------------------------------------------------------------------
3244 my $home_org = $patron->home_ou;
3245 my $req_org = $request_lib->id;
3247 $prox_cache{$home_org} =
3248 $e->search_actor_org_unit_proximity({from_org => $home_org})
3249 unless $prox_cache{$home_org};
3250 my $home_prox = $prox_cache{$home_org};
3251 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3254 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3255 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3257 my @keys = sort { $a <=> $b } keys %buckets;
3260 if( $home_org ne $req_org ) {
3261 # -----------------------------------------------------------------------
3262 # shove the copies close to the request_lib into the primary buckets
3263 # directly before the farthest away copies. That way, they are not
3264 # given priority, but they are checked before the farthest copies.
3265 # -----------------------------------------------------------------------
3266 $prox_cache{$req_org} =
3267 $e->search_actor_org_unit_proximity({from_org => $req_org})
3268 unless $prox_cache{$req_org};
3269 my $req_prox = $prox_cache{$req_org};
3272 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3273 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3275 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
3276 my $new_key = $highest_key - 0.5; # right before the farthest prox
3277 my @keys2 = sort { $a <=> $b } keys %buckets2;
3278 for my $key (@keys2) {
3279 last if $key >= $highest_key;
3280 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3284 @keys = sort { $a <=> $b } keys %buckets;
3289 my $age_protect_only = 0;
3290 OUTER: for my $key (@keys) {
3291 my @cps = @{$buckets{$key}};
3293 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3295 for my $copyid (@cps) {
3297 next if $seen{$copyid};
3298 $seen{$copyid} = 1; # there could be dupes given the merged buckets
3299 my $copy = $e->retrieve_asset_copy($copyid);
3300 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3302 unless($title) { # grab the title if we don't already have it
3303 my $vol = $e->retrieve_asset_call_number(
3304 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3305 $title = $vol->record;
3308 @status = verify_copy_for_hold(
3309 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3311 $age_protect_only ||= $status[3];
3312 last OUTER if $status[0];
3317 if (!defined($empty_ok)) {
3318 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3319 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3322 return (1,0) if ($empty_ok);
3324 $status[3] = $age_protect_only;
3329 sub _check_volume_hold_is_possible {
3330 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3331 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
3332 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
3333 $logger->info("checking possibility of volume hold for volume ".$vol->id);
3335 my $filter_copies = [];
3336 for my $copy (@$copies) {
3337 # ignore part-mapped copies for regular volume level holds
3338 push(@$filter_copies, $copy) unless
3339 new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
3341 $copies = $filter_copies;
3346 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3347 "payload" => {"fail_part" => "no_ultimate_items"}
3353 my $age_protect_only = 0;
3354 for my $copy ( @$copies ) {
3355 @status = verify_copy_for_hold(
3356 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
3357 $age_protect_only ||= $status[3];
3360 $status[3] = $age_protect_only;
3366 sub verify_copy_for_hold {
3367 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
3368 # $oargs should be undef unless we're overriding.
3369 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
3370 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3373 requestor => $requestor,
3376 title_descriptor => $title->fixed_fields,
3377 pickup_lib => $pickup_lib,
3378 request_lib => $request_lib,
3380 show_event_list => 1
3384 # Check for override permissions on events.
3385 if ($oargs && $permitted && scalar @$permitted) {
3386 # Remove the events from permitted that we can override.
3387 if ($oargs->{events}) {
3388 foreach my $evt (@{$oargs->{events}}) {
3389 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3392 # Now, we handle the override all case by checking remaining
3393 # events against override permissions.
3394 if (scalar @$permitted && $oargs->{all}) {
3395 # Pre-set events and failed members of oargs to empty
3396 # arrays, if they are not set, yet.
3397 $oargs->{events} = [] unless ($oargs->{events});
3398 $oargs->{failed} = [] unless ($oargs->{failed});
3399 # When we're done with these checks, we swap permitted
3400 # with a reference to @disallowed.
3401 my @disallowed = ();
3402 foreach my $evt (@{$permitted}) {
3403 # Check if we've already seen the event in this
3404 # session and it failed.
3405 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3406 push(@disallowed, $evt);
3408 # We have to check if the requestor has the
3409 # override permission.
3411 # AppUtils::check_user_perms returns the perm if
3412 # the user doesn't have it, undef if they do.
3413 if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3414 push(@disallowed, $evt);
3415 push(@{$oargs->{failed}}, $evt->{textcode});
3417 push(@{$oargs->{events}}, $evt->{textcode});
3421 $permitted = \@disallowed;
3425 my $age_protect_only = 0;
3426 if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3427 $age_protect_only = 1;
3431 (not scalar @$permitted), # true if permitted is an empty arrayref
3432 ( # XXX This test is of very dubious value; someone should figure
3433 # out what if anything is checking this value
3434 ($copy->circ_lib == $pickup_lib) and
3435 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3444 sub find_nearest_permitted_hold {
3447 my $editor = shift; # CStoreEditor object
3448 my $copy = shift; # copy to target
3449 my $user = shift; # staff
3450 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3452 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3454 my $bc = $copy->barcode;
3456 # find any existing holds that already target this copy
3457 my $old_holds = $editor->search_action_hold_request(
3458 { current_copy => $copy->id,
3459 cancel_time => undef,
3460 capture_time => undef
3464 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3466 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3467 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3469 my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3471 # the nearest_hold API call now needs this
3472 $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3473 unless ref $copy->call_number;
3475 # search for what should be the best holds for this copy to fulfill
3476 my $best_holds = $U->storagereq(
3477 "open-ils.storage.action.hold_request.nearest_hold.atomic",
3478 $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3480 # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3482 for my $holdid (@$old_holds) {
3483 next unless $holdid;
3484 push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3488 unless(@$best_holds) {
3489 $logger->info("circulator: no suitable holds found for copy $bc");
3490 return (undef, $evt);
3496 # for each potential hold, we have to run the permit script
3497 # to make sure the hold is actually permitted.
3500 for my $holdid (@$best_holds) {
3501 next unless $holdid;
3502 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3504 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3505 # Force and recall holds bypass all rules
3506 if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3510 my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3511 my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3513 $reqr_cache{$hold->requestor} = $reqr;
3514 $org_cache{$hold->request_lib} = $rlib;
3516 # see if this hold is permitted
3517 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3519 patron_id => $hold->usr,
3522 pickup_lib => $hold->pickup_lib,
3523 request_lib => $rlib,
3535 unless( $best_hold ) { # no "good" permitted holds were found
3537 $logger->info("circulator: no suitable holds found for copy $bc");
3538 return (undef, $evt);
3541 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3543 # indicate a permitted hold was found
3544 return $best_hold if $check_only;
3546 # we've found a permitted hold. we need to "grab" the copy
3547 # to prevent re-targeted holds (next part) from re-grabbing the copy
3548 $best_hold->current_copy($copy->id);
3549 $editor->update_action_hold_request($best_hold)
3550 or return (undef, $editor->event);
3555 # re-target any other holds that already target this copy
3556 for my $old_hold (@$old_holds) {
3557 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3558 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3559 $old_hold->id." after a better hold [".$best_hold->id."] was found");
3560 $old_hold->clear_current_copy;
3561 $old_hold->clear_prev_check_time;
3562 $editor->update_action_hold_request($old_hold)
3563 or return (undef, $editor->event);
3564 push(@retarget, $old_hold->id);
3567 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3575 __PACKAGE__->register_method(
3576 method => 'all_rec_holds',
3577 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3581 my( $self, $conn, $auth, $title_id, $args ) = @_;
3583 my $e = new_editor(authtoken=>$auth);
3584 $e->checkauth or return $e->event;
3585 $e->allowed('VIEW_HOLD') or return $e->event;
3588 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
3589 $args->{cancel_time} = undef;
3592 metarecord_holds => []
3594 , volume_holds => []
3596 , recall_holds => []
3599 , issuance_holds => []
3602 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3604 $resp->{metarecord_holds} = $e->search_action_hold_request(
3605 { hold_type => OILS_HOLD_TYPE_METARECORD,
3606 target => $mr_map->metarecord,
3612 $resp->{title_holds} = $e->search_action_hold_request(
3614 hold_type => OILS_HOLD_TYPE_TITLE,
3615 target => $title_id,
3619 my $parts = $e->search_biblio_monograph_part(
3625 $resp->{part_holds} = $e->search_action_hold_request(
3627 hold_type => OILS_HOLD_TYPE_MONOPART,
3633 my $subs = $e->search_serial_subscription(
3634 { record_entry => $title_id }, {idlist=>1});
3637 my $issuances = $e->search_serial_issuance(
3638 {subscription => $subs}, {idlist=>1}
3642 $resp->{issuance_holds} = $e->search_action_hold_request(
3644 hold_type => OILS_HOLD_TYPE_ISSUANCE,
3645 target => $issuances,
3652 my $vols = $e->search_asset_call_number(
3653 { record => $title_id, deleted => 'f' }, {idlist=>1});
3655 return $resp unless @$vols;
3657 $resp->{volume_holds} = $e->search_action_hold_request(
3659 hold_type => OILS_HOLD_TYPE_VOLUME,
3664 my $copies = $e->search_asset_copy(
3665 { call_number => $vols, deleted => 'f' }, {idlist=>1});
3667 return $resp unless @$copies;
3669 $resp->{copy_holds} = $e->search_action_hold_request(
3671 hold_type => OILS_HOLD_TYPE_COPY,
3676 $resp->{recall_holds} = $e->search_action_hold_request(
3678 hold_type => OILS_HOLD_TYPE_RECALL,
3683 $resp->{force_holds} = $e->search_action_hold_request(
3685 hold_type => OILS_HOLD_TYPE_FORCE,
3693 __PACKAGE__->register_method(
3694 method => 'stream_wide_holds',
3697 api_name => 'open-ils.circ.hold.wide_hash.stream'
3700 sub stream_wide_holds {
3701 my($self, $client, $auth, $restrictions, $order_by, $limit, $offset, $options) = @_;
3704 my $e = new_editor(authtoken=>$auth);
3705 $e->checkauth or return $e->event;
3706 $e->allowed('VIEW_HOLD') or return $e->event;
3708 if ($options->{recently_canceled}) {
3709 # Map the the recently canceled holds filter into values
3710 # wide-stream understands.
3711 my $filter = recently_canceled_holds_filter($e);
3712 $restrictions->{$_} =
3713 $filter->{where}->{$_} for keys %{$filter->{where}};
3715 $limit = $filter->{limit} if $filter->{limit};
3718 my $filters = OpenSRF::Utils::JSON->perl2JSON($restrictions);
3719 $logger->info("WIDE HOLD FILTERS: $filters");
3721 my $st = OpenSRF::AppSession->create('open-ils.storage');
3722 my $req = $st->request(
3723 'open-ils.storage.action.live_holds.wide_hash.atomic',
3724 $restrictions, $order_by, $limit, $offset
3727 my $results = $req->recv;
3732 if(UNIVERSAL::isa($results,"Error")) {
3733 throw OpenSRF::EX::ERROR ("Error fetch hold shelf list");
3736 my @rows = @{ $results->content };
3738 # Force immediate send of count response
3739 my $mbc = $client->max_bundle_count;
3740 $client->max_bundle_count(1);
3741 $client->respond(shift @rows);
3742 $client->max_bundle_count($mbc);
3744 foreach my $hold (@rows) {
3745 $client->respond($hold) if $hold;
3748 $client->respond_complete;
3754 __PACKAGE__->register_method(
3755 method => 'uber_hold',
3757 api_name => 'open-ils.circ.hold.details.retrieve'
3761 my($self, $client, $auth, $hold_id, $args) = @_;
3762 my $e = new_editor(authtoken=>$auth);
3763 $e->checkauth or return $e->event;
3764 return uber_hold_impl($e, $hold_id, $args);
3767 __PACKAGE__->register_method(
3768 method => 'batch_uber_hold',
3771 api_name => 'open-ils.circ.hold.details.batch.retrieve'
3774 sub batch_uber_hold {
3775 my($self, $client, $auth, $hold_ids, $args) = @_;
3776 my $e = new_editor(authtoken=>$auth);
3777 $e->checkauth or return $e->event;
3778 $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3782 sub uber_hold_impl {
3783 my($e, $hold_id, $args) = @_;
3786 my $flesh_fields = ['current_copy', 'usr', 'notes'];
3787 push (@$flesh_fields, 'requestor') if $args->{include_requestor};
3788 push (@$flesh_fields, 'cancel_cause') if $args->{include_cancel_cause};
3789 push (@$flesh_fields, 'sms_carrier') if $args->{include_sms_carrier};
3791 my $hold = $e->retrieve_action_hold_request([
3793 {flesh => 1, flesh_fields => {ahr => $flesh_fields}}
3794 ]) or return $e->event;
3796 if($hold->usr->id ne $e->requestor->id) {
3797 # caller is asking for someone else's hold
3798 $e->allowed('VIEW_HOLD') or return $e->event;
3799 $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3800 [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3803 # caller is asking for own hold, but may not have permission to view staff notes
3804 unless($e->allowed('VIEW_HOLD')) {
3805 $hold->notes( # filter out any staff notes (unless marked as public)
3806 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3810 my $user = $hold->usr;
3811 $hold->usr($user->id);
3814 my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args);
3816 flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3817 flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3819 my $details = retrieve_hold_queue_status_impl($e, $hold);
3820 $hold->usr($user) if $args->{include_usr}; # re-flesh
3825 ($copy ? (copy => $copy) : ()),
3826 ($volume ? (volume => $volume) : ()),
3827 ($issuance ? (issuance => $issuance) : ()),
3828 ($part ? (part => $part) : ()),
3829 ($args->{include_bre} ? (bre => $bre) : ()),
3830 ($args->{suppress_mvr} ? () : (mvr => $mvr)),
3834 $resp->{copy}->location(
3835 $e->retrieve_asset_copy_location($resp->{copy}->location))
3836 if $resp->{copy} and $args->{flesh_acpl};
3838 unless($args->{suppress_patron_details}) {
3839 my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3840 $resp->{patron_first} = $user->first_given_name,
3841 $resp->{patron_last} = $user->family_name,
3842 $resp->{patron_barcode} = $card->barcode,
3843 $resp->{patron_alias} = $user->alias,
3851 # -----------------------------------------------------
3852 # Returns the MVR object that represents what the
3854 # -----------------------------------------------------
3856 my( $e, $hold, $args ) = @_;
3864 my $no_mvr = $args->{suppress_mvr};
3866 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3867 $metarecord = $e->retrieve_metabib_metarecord($hold->target)
3868 or return $e->event;
3869 $tid = $metarecord->master_record;
3871 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3872 $tid = $hold->target;
3874 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3875 $volume = $e->retrieve_asset_call_number($hold->target)
3876 or return $e->event;
3877 $tid = $volume->record;
3879 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3880 $issuance = $e->retrieve_serial_issuance([
3882 {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3883 ]) or return $e->event;
3885 $tid = $issuance->subscription->record_entry;
3887 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3888 $part = $e->retrieve_biblio_monograph_part([
3890 ]) or return $e->event;
3892 $tid = $part->record;
3894 } 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 ) {
3895 $copy = $e->retrieve_asset_copy([
3897 {flesh => 1, flesh_fields => {acp => ['call_number']}}
3898 ]) or return $e->event;
3900 $volume = $copy->call_number;
3901 $tid = $volume->record;
3904 if(!$copy and ref $hold->current_copy ) {
3905 $copy = $hold->current_copy;
3906 $hold->current_copy($copy->id) unless $args->{include_current_copy};
3909 if(!$volume and $copy) {
3910 $volume = $e->retrieve_asset_call_number($copy->call_number);
3913 # TODO return metarcord mvr for M holds
3914 my $title = $e->retrieve_biblio_record_entry($tid);
3915 return ( ($no_mvr) ? undef : $U->record_to_mvr($title),
3916 $volume, $copy, $issuance, $part, $title, $metarecord);
3919 __PACKAGE__->register_method(
3920 method => 'clear_shelf_cache',
3921 api_name => 'open-ils.circ.hold.clear_shelf.get_cache',
3925 Returns the holds processed with the given cache key
3930 sub clear_shelf_cache {
3931 my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3932 my $e = new_editor(authtoken => $auth, xact => 1);
3933 return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3936 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3938 my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3941 $logger->info("no hold data found in cache"); # XXX TODO return event
3947 foreach (keys %$hold_data) {
3948 $maximum += scalar(@{ $hold_data->{$_} });
3950 $client->respond({"maximum" => $maximum, "progress" => 0});
3952 for my $action (sort keys %$hold_data) {
3953 while (@{$hold_data->{$action}}) {
3954 my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3956 my $result_chunk = $e->json_query({
3958 "acp" => ["barcode"],
3960 first_given_name second_given_name family_name alias
3963 "acnp" => [{column => "label", alias => "prefix"}],
3964 "acns" => [{column => "label", alias => "suffix"}],
3972 "field" => "id", "fkey" => "current_copy",
3975 "field" => "id", "fkey" => "call_number",
3978 "field" => "id", "fkey" => "record"
3981 "field" => "id", "fkey" => "prefix"
3984 "field" => "id", "fkey" => "suffix"
3988 "acpl" => {"field" => "id", "fkey" => "location"}
3991 "au" => {"field" => "id", "fkey" => "usr"}
3994 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3995 }, {"substream" => 1}) or return $e->die_event;
3999 +{"action" => $action, "hold_details" => $_}
4010 __PACKAGE__->register_method(
4011 method => 'clear_shelf_process',
4013 api_name => 'open-ils.circ.hold.clear_shelf.process',
4016 1. Find all holds that have expired on the holds shelf
4018 3. If a clear-shelf status is configured, put targeted copies into this status
4019 4. Divide copies into 3 groups: items to transit, items to reshelve, and items
4020 that are needed for holds. No subsequent action is taken on the holds
4021 or items after grouping.
4026 sub clear_shelf_process {
4027 my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
4029 my $e = new_editor(authtoken=>$auth);
4030 $e->checkauth or return $e->die_event;
4031 my $cache = OpenSRF::Utils::Cache->new('global');
4033 $org_id ||= $e->requestor->ws_ou;
4034 $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
4036 my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
4038 my @hold_ids = $self->method_lookup(
4039 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
4040 )->run($auth, $org_id, $match_copy);
4045 my @canceled_holds; # newly canceled holds
4046 $chunk_size ||= 25; # chunked status updates
4047 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
4050 for my $hold_id (@hold_ids) {
4052 $logger->info("Clear shelf processing hold $hold_id");
4054 my $hold = $e->retrieve_action_hold_request([
4057 flesh_fields => {ahr => ['current_copy']}
4061 if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
4062 $hold->cancel_time('now');
4063 $hold->cancel_cause(2); # Hold Shelf expiration
4064 $e->update_action_hold_request($hold) or return $e->die_event;
4065 push(@canceled_holds, $hold_id);
4068 my $copy = $hold->current_copy;
4070 if($copy_status or $copy_status == 0) {
4071 # if a clear-shelf copy status is defined, update the copy
4072 $copy->status($copy_status);
4073 $copy->edit_date('now');
4074 $copy->editor($e->requestor->id);
4075 $e->update_asset_copy($copy) or return $e->die_event;
4078 push(@holds, $hold);
4079 $client->respond({maximum => int(scalar(@holds)), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
4088 pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
4091 for my $hold (@holds) {
4093 my $copy = $hold->current_copy;
4094 my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
4096 if($alt_hold and !$match_copy) {
4098 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
4100 } elsif($copy->circ_lib != $e->requestor->ws_ou) {
4102 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
4106 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
4110 my $cache_key = md5_hex(time . $$ . rand());
4111 $logger->info("clear_shelf_cache: storing under $cache_key");
4112 $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours. configurable?
4114 # tell the client we're done
4115 $client->respond_complete({cache_key => $cache_key});
4118 # fire off the hold cancelation trigger and wait for response so don't flood the service
4120 # refetch the holds to pick up the caclulated cancel_time,
4121 # which may be needed by Action/Trigger
4123 my $updated_holds = [];
4124 $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
4127 $U->create_events_for_hook(
4128 'hold_request.cancel.expire_holds_shelf',
4129 $_, $org_id, undef, undef, 1) for @$updated_holds;
4132 # tell the client we're done
4133 $client->respond_complete;
4137 # returns IDs for holds that are on the holds shelf but
4138 # have had their pickup_libs change while on the shelf.
4139 sub pickup_lib_changed_on_shelf_holds {
4142 my $ignore_holds = shift;
4143 $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
4146 select => { alhr => ['id'] },
4151 fkey => 'current_copy'
4156 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
4158 capture_time => { "!=" => undef },
4159 fulfillment_time => undef,
4160 current_shelf_lib => $org_id,
4161 pickup_lib => {'!=' => {'+alhr' => 'current_shelf_lib'}}
4166 $query->{where}->{'+alhr'}->{id} =
4167 {'not in' => $ignore_holds} if @$ignore_holds;
4169 my $hold_ids = $e->json_query($query);
4170 return [ map { $_->{id} } @$hold_ids ];
4173 __PACKAGE__->register_method(
4174 method => 'usr_hold_summary',
4175 api_name => 'open-ils.circ.holds.user_summary',
4177 Returns a summary of holds statuses for a given user
4181 sub usr_hold_summary {
4182 my($self, $conn, $auth, $user_id) = @_;
4184 my $e = new_editor(authtoken=>$auth);
4185 $e->checkauth or return $e->event;
4186 $e->allowed('VIEW_HOLD') or return $e->event;
4188 my $holds = $e->search_action_hold_request(
4191 fulfillment_time => undef,
4192 cancel_time => undef,
4196 my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
4197 $summary{_hold_status($e, $_)} += 1 for @$holds;
4203 __PACKAGE__->register_method(
4204 method => 'hold_has_copy_at',
4205 api_name => 'open-ils.circ.hold.has_copy_at',
4208 'Returns the ID of the found copy and name of the shelving location if there is ' .
4209 'an available copy at the specified org unit. Returns empty hash otherwise. ' .
4210 'The anticipated use for this method is to determine whether an item is ' .
4211 'available at the library where the user is placing the hold (or, alternatively, '.
4212 'at the pickup library) to encourage bypassing the hold placement and just ' .
4213 'checking out the item.' ,
4215 { desc => 'Authentication Token', type => 'string' },
4216 { desc => 'Method Arguments. Options include: hold_type, hold_target, org_unit. '
4217 . 'hold_type is the hold type code (T, V, C, M, ...). '
4218 . 'hold_target is the identifier of the hold target object. '
4219 . 'org_unit is org unit ID.',
4224 desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
4230 sub hold_has_copy_at {
4231 my($self, $conn, $auth, $args) = @_;
4233 my $e = new_editor(authtoken=>$auth);
4234 $e->checkauth or return $e->event;
4236 my $hold_type = $$args{hold_type};
4237 my $hold_target = $$args{hold_target};
4238 my $org_unit = $$args{org_unit};
4241 select => {acp => ['id'], acpl => ['name']},
4246 filter => { holdable => 't', deleted => 'f' },
4249 ccs => {field => 'id', filter => {holdable => 't', is_available => 't'}, fkey => 'status'}
4252 where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit }},
4256 if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
4258 $query->{where}->{'+acp'}->{id} = $hold_target;
4260 } elsif($hold_type eq 'V') {
4262 $query->{where}->{'+acp'}->{call_number} = $hold_target;
4264 } elsif($hold_type eq 'P') {
4266 $query->{from}->{acp}->{acpm} = {
4267 field => 'target_copy',
4269 filter => {part => $hold_target},
4272 } elsif($hold_type eq 'I') {
4274 $query->{from}->{acp}->{sitem} = {
4277 filter => {issuance => $hold_target},
4280 } elsif($hold_type eq 'T') {
4282 $query->{from}->{acp}->{acn} = {
4284 fkey => 'call_number',
4288 filter => {id => $hold_target},
4296 $query->{from}->{acp}->{acn} = {
4298 fkey => 'call_number',
4307 filter => {metarecord => $hold_target},
4315 my $res = $e->json_query($query)->[0] or return {};
4316 return {copy => $res->{id}, location => $res->{name}} if $res;
4320 # returns true if the user already has an item checked out
4321 # that could be used to fulfill the requested hold.
4322 sub hold_item_is_checked_out {
4323 my($e, $user_id, $hold_type, $hold_target) = @_;
4326 select => {acp => ['id']},
4327 from => {acp => {}},
4331 in => { # copies for circs the user has checked out
4332 select => {circ => ['target_copy']},
4336 checkin_time => undef,
4338 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
4339 {stop_fines => undef}
4349 if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
4351 $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
4353 } elsif($hold_type eq 'V') {
4355 $query->{where}->{'+acp'}->{call_number} = $hold_target;
4357 } elsif($hold_type eq 'P') {
4359 $query->{from}->{acp}->{acpm} = {
4360 field => 'target_copy',
4362 filter => {part => $hold_target},
4365 } elsif($hold_type eq 'I') {
4367 $query->{from}->{acp}->{sitem} = {
4370 filter => {issuance => $hold_target},
4373 } elsif($hold_type eq 'T') {
4375 $query->{from}->{acp}->{acn} = {
4377 fkey => 'call_number',
4381 filter => {id => $hold_target},
4389 $query->{from}->{acp}->{acn} = {
4391 fkey => 'call_number',
4400 filter => {metarecord => $hold_target},
4408 return $e->json_query($query)->[0];
4411 __PACKAGE__->register_method(
4412 method => 'change_hold_title',
4413 api_name => 'open-ils.circ.hold.change_title',
4416 Updates all title level holds targeting the specified bibs to point a new bib./,
4418 { desc => 'Authentication Token', type => 'string' },
4419 { desc => 'New Target Bib Id', type => 'number' },
4420 { desc => 'Old Target Bib Ids', type => 'array' },
4422 return => { desc => '1 on success' }
4426 __PACKAGE__->register_method(
4427 method => 'change_hold_title_for_specific_holds',
4428 api_name => 'open-ils.circ.hold.change_title.specific_holds',
4431 Updates specified holds to target new bib./,
4433 { desc => 'Authentication Token', type => 'string' },
4434 { desc => 'New Target Bib Id', type => 'number' },
4435 { desc => 'Holds Ids for holds to update', type => 'array' },
4437 return => { desc => '1 on success' }
4442 sub change_hold_title {
4443 my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4445 my $e = new_editor(authtoken=>$auth, xact=>1);
4446 return $e->die_event unless $e->checkauth;
4448 my $holds = $e->search_action_hold_request(
4451 capture_time => undef,
4452 cancel_time => undef,
4453 fulfillment_time => undef,
4459 flesh_fields => { ahr => ['usr'] }
4465 for my $hold (@$holds) {
4466 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4467 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4468 $hold->target( $new_bib_id );
4469 $e->update_action_hold_request($hold) or return $e->die_event;
4474 _reset_hold($self, $e->requestor, $_) for @$holds;
4479 sub change_hold_title_for_specific_holds {
4480 my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4482 my $e = new_editor(authtoken=>$auth, xact=>1);
4483 return $e->die_event unless $e->checkauth;
4485 my $holds = $e->search_action_hold_request(
4488 capture_time => undef,
4489 cancel_time => undef,
4490 fulfillment_time => undef,
4496 flesh_fields => { ahr => ['usr'] }
4502 for my $hold (@$holds) {
4503 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4504 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4505 $hold->target( $new_bib_id );
4506 $e->update_action_hold_request($hold) or return $e->die_event;
4511 _reset_hold($self, $e->requestor, $_) for @$holds;
4516 __PACKAGE__->register_method(
4517 method => 'rec_hold_count',
4518 api_name => 'open-ils.circ.bre.holds.count',
4520 desc => q/Returns the total number of holds that target the
4521 selected bib record or its associated copies and call_numbers/,
4523 { desc => 'Bib ID', type => 'number' },
4524 { desc => q/Optional arguments. Supported arguments include:
4525 "pickup_lib_descendant" -> limit holds to those whose pickup
4526 library is equal to or is a child of the provided org unit/,
4530 return => {desc => 'Hold count', type => 'number'}
4534 __PACKAGE__->register_method(
4535 method => 'rec_hold_count',
4536 api_name => 'open-ils.circ.mmr.holds.count',
4538 desc => q/Returns the total number of holds that target the
4539 selected metarecord or its associated copies, call_numbers, and bib records/,
4541 { desc => 'Metarecord ID', type => 'number' },
4543 return => {desc => 'Hold count', type => 'number'}
4547 # XXX Need to add type I holds to these counts
4548 sub rec_hold_count {
4549 my($self, $conn, $target_id, $args) = @_;
4556 filter => {metarecord => $target_id}
4563 filter => { id => $target_id },
4568 if($self->api_name =~ /mmr/) {
4569 delete $bre_join->{bre}->{filter};
4570 $bre_join->{bre}->{join} = $mmr_join;
4576 fkey => 'call_number',
4582 select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4586 cancel_time => undef,
4587 fulfillment_time => undef,
4591 hold_type => [qw/C F R/],
4594 select => {acp => ['id']},
4595 from => { acp => $cn_join }
4605 select => {acn => ['id']},
4606 from => {acn => $bre_join}
4616 select => {bmp => ['id']},
4617 from => {bmp => $bre_join}
4625 target => $target_id
4633 if($self->api_name =~ /mmr/) {
4634 $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4639 select => {bre => ['id']},
4640 from => {bre => $mmr_join}
4646 $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4649 target => $target_id
4655 if (my $pld = $args->{pickup_lib_descendant}) {
4657 my $top_ou = new_editor()->search_actor_org_unit(
4658 {parent_ou => undef}
4659 )->[0]; # XXX Assumes single root node. Not alone in this...
4661 $query->{where}->{'+ahr'}->{pickup_lib} = {
4663 select => {aou => [{
4665 transform => 'actor.org_unit_descendants',
4666 result_field => 'id'
4669 where => {id => $pld}
4671 } if ($pld != $top_ou->id);
4674 # To avoid Internal Server Errors, we get an editor, then run the
4675 # query and check the result. If anything fails, we'll return 0.
4677 if (my $e = new_editor()) {
4678 my $query_result = $e->json_query($query);
4679 if ($query_result && @{$query_result}) {
4680 $result = $query_result->[0]->{count}
4687 # A helper function to calculate a hold's expiration time at a given
4688 # org_unit. Takes the org_unit as an argument and returns either the
4689 # hold expire time as an ISO8601 string or undef if there is no hold
4690 # expiration interval set for the subject ou.
4691 sub calculate_expire_time
4694 my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4696 my $date = DateTime->now->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($interval));
4697 return $U->epoch2ISO8601($date->epoch);
4703 __PACKAGE__->register_method(
4704 method => 'mr_hold_filter_attrs',
4705 api_name => 'open-ils.circ.mmr.holds.filters',
4710 Returns the set of available formats and languages for the
4711 constituent records of the provided metarcord.
4712 If an array of hold IDs is also provided, information about
4713 each is returned as well. This information includes:
4714 1. a slightly easier to read version of holdable_formats
4715 2. attributes describing the set of format icons included
4716 in the set of desired, constituent records.
4719 {desc => 'Metarecord ID', type => 'number'},
4720 {desc => 'Context Org ID', type => 'number'},
4721 {desc => 'Hold ID List', type => 'array'},
4725 Stream of objects. The first will have a 'metarecord' key
4726 containing non-hold-specific metarecord information, subsequent
4727 responses will contain a 'hold' key containing hold-specific
4735 sub mr_hold_filter_attrs {
4736 my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4737 my $e = new_editor();
4739 # by default, return MR / hold attributes for all constituent
4740 # records with holdable copies. If there is a hard boundary,
4741 # though, limit to records with copies within the boundary,
4742 # since anything outside the boundary can never be held.
4745 $org_depth = $U->ou_ancestor_setting_value(
4746 $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4749 # get all org-scoped records w/ holdable copies for this metarecord
4750 my ($bre_ids) = $self->method_lookup(
4751 'open-ils.circ.holds.metarecord.filtered_records')->run(
4752 $mr_id, undef, $org_id, $org_depth);
4754 my $item_lang_attr = 'item_lang'; # configurable?
4755 my $format_attr = $e->retrieve_config_global_flag(
4756 'opac.metarecord.holds.format_attr')->value;
4758 # helper sub for fetching ccvms for a batch of record IDs
4759 sub get_batch_ccvms {
4760 my ($e, $attr, $bre_ids) = @_;
4761 return [] unless $bre_ids and @$bre_ids;
4762 my $vals = $e->search_metabib_record_attr_flat({
4766 return [] unless @$vals;
4767 return $e->search_config_coded_value_map({
4769 code => [map {$_->value} @$vals]
4773 my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4774 my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4779 formats => $formats,
4784 return unless $hold_ids;
4785 my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4786 $icon_attr = $icon_attr ? $icon_attr->value : '';
4788 for my $hold_id (@$hold_ids) {
4789 my $hold = $e->retrieve_action_hold_request($hold_id)
4790 or return $e->event;
4792 next unless $hold->hold_type eq 'M';
4802 # collect the ccvm's for the selected formats / language
4803 # (i.e. the holdable formats) on the MR.
4804 # this assumes a two-key structure for format / language,
4805 # though no assumption is made about the keys themselves.
4806 my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4808 my $format_vals = [];
4809 for my $val (values %$hformats) {
4810 # val is either a single ccvm or an array of them
4811 $val = [$val] unless ref $val eq 'ARRAY';
4812 for my $node (@$val) {
4813 push (@$lang_vals, $node->{_val})
4814 if $node->{_attr} eq $item_lang_attr;
4815 push (@$format_vals, $node->{_val})
4816 if $node->{_attr} eq $format_attr;
4820 # fetch the ccvm's for consistency with the {metarecord} blob
4821 $resp->{hold}{formats} = $e->search_config_coded_value_map({
4822 ctype => $format_attr, code => $format_vals});
4823 $resp->{hold}{langs} = $e->search_config_coded_value_map({
4824 ctype => $item_lang_attr, code => $lang_vals});
4826 # find all of the bib records within this metarcord whose
4827 # format / language match the holdable formats on the hold
4828 my ($bre_ids) = $self->method_lookup(
4829 'open-ils.circ.holds.metarecord.filtered_records')->run(
4830 $hold->target, $hold->holdable_formats,
4831 $hold->selection_ou, $hold->selection_depth);
4833 # now find all of the 'icon' attributes for the records
4834 $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4835 $client->respond($resp);
4841 __PACKAGE__->register_method(
4842 method => "copy_has_holds_count",
4843 api_name => "open-ils.circ.copy.has_holds_count",
4847 Returns the number of holds a paticular copy has
4850 { desc => 'Authentication Token', type => 'string'},
4851 { desc => 'Copy ID', type => 'number'}
4862 sub copy_has_holds_count {
4863 my( $self, $conn, $auth, $copyid ) = @_;
4864 my $e = new_editor(authtoken=>$auth);
4865 return $e->event unless $e->checkauth;
4867 if( $copyid && $copyid > 0 ) {
4868 my $meth = 'retrieve_action_has_holds_count';
4869 my $data = $e->$meth($copyid);
4871 return $data->count();
4877 __PACKAGE__->register_method(
4878 method => "retrieve_holds_by_usr_notify_value_staff",
4879 api_name => "open-ils.circ.holds.retrieve_by_notify_staff",
4881 desc => "Retrieve the hold, for the specified user using the notify value. $ses_is_req_note",
4883 { desc => 'Authentication token', type => 'string' },
4884 { desc => 'User ID', type => 'number' },
4885 { desc => 'notify value', type => 'string' },
4886 { desc => 'notify_type', type => 'string' }
4889 desc => 'Hold objects with transits attached, event on error',
4894 sub retrieve_holds_by_usr_notify_value_staff {
4896 my($self, $conn, $auth, $usr_id, $contact, $cType) = @_;
4898 my $e = new_editor(authtoken=>$auth);
4899 $e->checkauth or return $e->event;
4901 if ($e->requestor->id != $usr_id){
4902 $e->allowed('VIEW_HOLD') or return $e->event;
4906 "select" => { "ahr" => ["id", "sms_notify", "phone_notify", "email_notify", "sms_carrier"]},
4910 "capture_time" => undef,
4911 "cancel_time" => undef,
4912 "fulfillment_time" => undef,
4916 if ($cType eq "day_phone" or $cType eq "evening_phone" or
4917 $cType eq "other_phone" or $cType eq "default_phone"){
4918 $q->{where}->{"-not"} = [
4919 { "phone_notify" => { "=" => $contact} },
4920 { "phone_notify" => { "<>" => undef } }
4925 if ($cType eq "default_sms") {
4926 $q->{where}->{"-not"} = [
4927 { "sms_notify" => { "=" => $contact} },
4928 { "sms_notify" => { "<>" => undef } }
4932 if ($cType eq "default_sms_carrier_id") {
4933 $q->{where}->{"-not"} = [
4934 { "sms_carrier" => { "=" => int($contact)} },
4935 { "sms_carrier" => { "<>" => undef } }
4939 if ($cType =~ /notify/){
4940 # this is was notification pref change
4941 # we find all unfulfilled holds that match have that pref
4942 my $optr = $contact == 1 ? "<>" : "="; # unless it's email, true val means we want to query for not null
4943 my $conj = $optr eq '=' ? '-or' : '-and';
4944 if ($cType =~ /sms/) {
4945 $q->{where}->{$conj} = [ { sms_notify => { $optr => undef } }, { sms_notify => { $optr => '' } } ];
4947 if ($cType =~ /phone/) {
4948 $q->{where}->{$conj} = [ { phone_notify => { $optr => undef } }, { phone_notify => { $optr => '' } } ];
4950 if ($cType =~ /email/) {
4952 $q->{where}->{'+ahr'} = 'email_notify';
4954 $q->{where}->{'-not'} = {'+ahr' => 'email_notify'};
4959 my $holds = $e->json_query($q);
4960 #$hold_ids = [ map { $_->{id} } @$hold_ids ];
4965 __PACKAGE__->register_method(
4966 method => "batch_update_holds_by_value_staff",
4967 api_name => "open-ils.circ.holds.batch_update_holds_by_notify_staff",
4969 desc => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
4971 { desc => 'Authentication token', type => 'string' },
4972 { desc => 'User ID', type => 'number' },
4973 { desc => 'Hold IDs', type => 'array' },
4974 { desc => 'old notify value', type => 'string' },
4975 { desc => 'new notify value', type => 'string' },
4976 { desc => 'field name', type => 'string' },
4977 { desc => 'SMS carrier ID', type => 'number' }
4981 desc => 'Hold objects with transits attached, event on error',
4986 sub batch_update_holds_by_value_staff {
4987 my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType, $carrierId) = @_;
4989 my $e = new_editor(authtoken=>$auth, xact=>1);
4990 $e->checkauth or return $e->event;
4991 if ($e->requestor->id != $usr_id){
4992 $e->allowed('UPDATE_HOLD') or return $e->event;
4996 for my $id (@$hold_ids) {
4998 my $hold = $e->retrieve_action_hold_request($id);
5000 if ($cType eq "day_phone" or $cType eq "evening_phone" or
5001 $cType eq "other_phone" or $cType eq "default_phone") {
5003 if ($newval eq '') {
5004 $hold->clear_phone_notify();
5007 $hold->phone_notify($newval);
5011 if ($cType eq "default_sms"){
5012 if ($newval eq '') {
5013 $hold->clear_sms_notify();
5014 $hold->clear_sms_carrier(); # TODO: prevent orphan sms_carrier, via db trigger
5017 $hold->sms_notify($newval);
5018 $hold->sms_carrier($carrierId);
5023 if ($cType eq "default_sms_carrier_id") {
5024 $hold->sms_carrier($newval);
5027 if ($cType =~ /notify/){
5028 # this is a notification pref change
5029 if ($cType =~ /email/) { $hold->email_notify($newval); }
5030 if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
5031 if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
5032 # the other case, where x_notify is changed to true,
5033 # is covered by an actual value being assigned
5036 $e->update_action_hold_request($hold) or return $e->die_event;
5041 $e->commit; #unless $U->event_code($res);
5047 __PACKAGE__->register_method(
5048 method => "retrieve_holds_by_usr_with_notify",
5049 api_name => "open-ils.circ.holds.retrieve.by_usr.with_notify",
5051 desc => "Retrieve the hold, for the specified user using the notify value. $ses_is_req_note",
5053 { desc => 'Authentication token', type => 'string' },
5054 { desc => 'User ID', type => 'number' },
5057 desc => 'Lists of holds with notification values, event on error',
5062 sub retrieve_holds_by_usr_with_notify {
5064 my($self, $conn, $auth, $usr_id) = @_;
5066 my $e = new_editor(authtoken=>$auth);
5067 $e->checkauth or return $e->event;
5069 if ($e->requestor->id != $usr_id){
5070 $e->allowed('VIEW_HOLD') or return $e->event;
5074 "select" => { "ahr" => ["id", "phone_notify", "email_notify", "sms_carrier", "sms_notify"]},
5078 "capture_time" => undef,
5079 "cancel_time" => undef,
5080 "fulfillment_time" => undef,
5084 my $holds = $e->json_query($q);
5088 __PACKAGE__->register_method(
5089 method => "batch_update_holds_by_value",
5090 api_name => "open-ils.circ.holds.batch_update_holds_by_notify",
5092 desc => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
5094 { desc => 'Authentication token', type => 'string' },
5095 { desc => 'User ID', type => 'number' },
5096 { desc => 'Hold IDs', type => 'array' },
5097 { desc => 'old notify value', type => 'string' },
5098 { desc => 'new notify value', type => 'string' },
5099 { desc => 'notify_type', type => 'string' }
5102 desc => 'Hold objects with transits attached, event on error',
5107 sub batch_update_holds_by_value {
5108 my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType) = @_;
5110 my $e = new_editor(authtoken=>$auth, xact=>1);
5111 $e->checkauth or return $e->event;
5112 if ($e->requestor->id != $usr_id){
5113 $e->allowed('UPDATE_HOLD') or return $e->event;
5117 for my $id (@$hold_ids) {
5119 my $hold = $e->retrieve_action_hold_request(int($id));
5121 if ($cType eq "day_phone" or $cType eq "evening_phone" or
5122 $cType eq "other_phone" or $cType eq "default_phone") {
5123 # change phone number value on hold
5124 $hold->phone_notify($newval);
5126 if ($cType eq "default_sms") {
5127 # change SMS number value on hold
5128 $hold->sms_notify($newval);
5131 if ($cType eq "default_sms_carrier_id") {
5132 $hold->sms_carrier(int($newval));
5135 if ($cType =~ /notify/){
5136 # this is a notification pref change
5137 if ($cType =~ /email/) { $hold->email_notify($newval); }
5138 if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
5139 if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
5140 # the other case, where x_notify is changed to true,
5141 # is covered by an actual value being assigned
5144 $e->update_action_hold_request($hold) or return $e->die_event;
5149 $e->commit; #unless $U->event_code($res);
5153 __PACKAGE__->register_method(
5154 method => "hold_metadata",
5155 api_name => "open-ils.circ.hold.get_metadata",
5160 Returns a stream of objects containing whatever bib,
5161 volume, etc. data is available to the specific hold
5165 {desc => 'Hold Type', type => 'string'},
5166 {desc => 'Hold Target(s)', type => 'number or array'},
5167 {desc => 'Context org unit (optional)', type => 'number'}
5171 Stream of hold metadata objects.
5180 my ($self, $client, $hold_type, $hold_targets, $org_id) = @_;
5182 $hold_targets = [$hold_targets] unless ref $hold_targets;
5184 my $e = new_editor();
5185 for my $target (@$hold_targets) {
5187 # create a dummy hold for find_hold_mvr
5188 my $hold = Fieldmapper::action::hold_request->new;
5189 $hold->hold_type($hold_type);
5190 $hold->target($target);
5192 my (undef, $volume, $copy, $issuance, $part, $bre, $metarecord) =
5193 find_hold_mvr($e, $hold, {suppress_mvr => 1});
5195 $bre->clear_marc; # avoid bulk
5201 issuance => $issuance,
5204 part_required => 'f',
5206 metarecord => $metarecord,
5207 metarecord_filters => {}
5210 # If this is a bib hold or metarecord hold, also return the
5211 # available set of MR filters (AKA "Holdable Formats") for the
5212 # hold. For bib holds these may be used to upgrade the hold
5213 # from a bib to metarecord hold.
5214 if ($hold_type eq 'T') {
5215 my $map = $e->search_metabib_metarecord_source_map(
5216 {source => $meta->{bibrecord}->id})->[0];
5219 $meta->{metarecord} =
5220 $e->retrieve_metabib_metarecord($map->metarecord);
5223 # Also fetch the available parts for bib-level holds.
5224 $meta->{parts} = $e->search_biblio_monograph_part(
5226 {record => $bre->id, deleted => 'f'},
5227 {order_by => {bmp => 'label_sortkey'}}
5231 # T holds on records that have parts are normally OK, but if the record has
5232 # no non-part copies, the hold will ultimately fail. When that happens,
5233 # require the user to select a part.
5235 # If the global flag circ.holds.api_require_monographic_part_when_present is
5236 # enabled, or the library setting circ.holds.ui_require_monographic_part_when_present
5237 # is true for any involved owning_library, then also require part selection.
5238 my $part_required = 0;
5239 if ($meta->{parts}) {
5240 my $part_required_flag = $e->retrieve_config_global_flag('circ.holds.api_require_monographic_part_when_present');
5241 $part_required = ($part_required_flag and $U->is_true($part_required_flag->enabled));
5242 if (!$part_required) {
5243 my $resp = $e->json_query({
5245 acn => ['owning_lib']
5247 from => {acn => {acp => {type => 'left'}}},
5252 {id => undef} # left join
5255 '+acn' => {deleted => 'f', record => $bre->id}
5259 my $org_ids = [map {$_->{owning_lib}} @$resp];
5260 foreach my $org (@$org_ids) { # FIXME: worth shortcutting/optimizing?
5261 if ($U->ou_ancestor_setting_value($org, 'circ.holds.ui_require_monographic_part_when_present')) {
5266 if (!$part_required) {
5267 my $np_copies = $e->json_query({
5268 select => { acp => [{column => 'id', transform => 'count', alias => 'count'}]},
5269 from => {acp => {acn => {}, acpm => {type => 'left'}}},
5271 '+acp' => {deleted => 'f'},
5272 '+acn' => {deleted => 'f', record => $bre->id},
5273 '+acpm' => {id => undef}
5276 $part_required = 1 if $np_copies->[0]->{count} == 0;
5279 $meta->{part_required} = $part_required;
5282 if ($meta->{metarecord}) {
5285 $self->method_lookup('open-ils.circ.mmr.holds.filters')
5286 ->run($meta->{metarecord}->id, $org_id);
5289 $meta->{metarecord_filters} = $filters->{metarecord};
5293 $client->respond($meta);