1 # ---------------------------------------------------------------
2 # Copyright (C) 2005 Georgia Public Library Service
3 # Bill Erickson <highfalutin@gmail.com>
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 # ---------------------------------------------------------------
17 package OpenILS::Application::Circ::Holds;
18 use base qw/OpenILS::Application/;
19 use strict; use warnings;
20 use List::Util qw(shuffle);
21 use OpenILS::Application::AppUtils;
24 use OpenSRF::EX qw(:try);
28 use OpenSRF::Utils::Logger qw(:logger);
29 use OpenILS::Utils::CStoreEditor q/:funcs/;
30 use OpenILS::Utils::PermitHold;
31 use OpenSRF::Utils::SettingsClient;
32 use OpenILS::Const qw/:const/;
33 use OpenILS::Application::Circ::Transit;
34 use OpenILS::Application::Actor::Friends;
36 use DateTime::Format::ISO8601;
37 use OpenILS::Utils::DateTime qw/:datetime/;
38 use Digest::MD5 qw(md5_hex);
39 use OpenSRF::Utils::Cache;
40 use OpenSRF::Utils::JSON;
41 my $apputils = "OpenILS::Application::AppUtils";
44 __PACKAGE__->register_method(
45 method => "test_and_create_hold_batch",
46 api_name => "open-ils.circ.holds.test_and_create.batch",
49 desc => q/This is for batch creating a set of holds where every field is identical except for the targets./,
51 { desc => 'Authentication token', type => 'string' },
52 { desc => 'Hash of named parameters. Same as for open-ils.circ.title_hold.is_possible, though the pertinent target field is automatically populated based on the hold_type and the specified list of targets.', type => 'object'},
53 { desc => 'Array of target ids', type => 'array' }
56 desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
61 __PACKAGE__->register_method(
62 method => "test_and_create_hold_batch",
63 api_name => "open-ils.circ.holds.test_and_create.batch.override",
66 desc => '@see open-ils.circ.holds.test_and_create.batch',
71 sub test_and_create_hold_batch {
72 my( $self, $conn, $auth, $params, $target_list, $oargs ) = @_;
75 if ($self->api_name =~ /override/) {
77 $oargs = { all => 1 } unless defined $oargs;
78 $$params{oargs} = $oargs; # for is_possible checking.
81 my $e = new_editor(authtoken=>$auth);
82 return $e->die_event unless $e->checkauth;
83 $$params{'requestor'} = $e->requestor->id;
86 if ($$params{'hold_type'} eq 'T') { $target_field = 'titleid'; }
87 elsif ($$params{'hold_type'} eq 'C') { $target_field = 'copy_id'; }
88 elsif ($$params{'hold_type'} eq 'R') { $target_field = 'copy_id'; }
89 elsif ($$params{'hold_type'} eq 'F') { $target_field = 'copy_id'; }
90 elsif ($$params{'hold_type'} eq 'I') { $target_field = 'issuanceid'; }
91 elsif ($$params{'hold_type'} eq 'V') { $target_field = 'volume_id'; }
92 elsif ($$params{'hold_type'} eq 'M') { $target_field = 'mrid'; }
93 elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
94 else { return undef; }
96 my $formats_map = delete $$params{holdable_formats_map};
98 foreach (@$target_list) {
99 $$params{$target_field} = $_;
101 # copy the requested formats from the target->formats map
102 # into the top-level formats attr for each hold
103 $$params{holdable_formats} = $formats_map->{$_};
106 ($res) = $self->method_lookup(
107 'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
108 if ($res->{'success'} == 1 || ($override && $res->{place_unfillable})) {
110 $params->{'depth'} = $res->{'depth'} if $res->{'depth'};
112 if ($$oargs{honor_user_settings}) {
113 my $recipient = $e->retrieve_actor_user($$params{patronid})
114 or return $e->die_event;
115 my $opac_hold_notify = $e->search_actor_user_setting(
116 {usr => $$params{patronid}, name => 'opac.hold_notify'})->[0];
117 if ($opac_hold_notify) {
118 if ($opac_hold_notify->value =~ 'email') {
119 $$params{email_notify} = 1;
121 if ($opac_hold_notify->value =~ 'phone') {
122 my $opac_default_phone = $e->search_actor_user_setting(
123 {usr => $$params{patronid}, name => 'opac.default_phone'})->[0];
124 # FIXME - what's up with the ->value putting quotes around the string?
125 if ($opac_default_phone && $opac_default_phone->value =~ /^"(.*)"$/) {
126 $$params{phone_notify} = $1;
129 if ($opac_hold_notify->value =~ 'sms') {
130 my $opac_default_sms_carrier = $e->search_actor_user_setting(
131 {usr => $$params{patronid}, name => 'opac.default_sms_carrier'})->[0];
132 $$params{sms_carrier} = $opac_default_sms_carrier->value if $opac_default_sms_carrier;
133 my $opac_default_sms_notify = $e->search_actor_user_setting(
134 {usr => $$params{patronid}, name => 'opac.default_sms_notify'})->[0];
135 if ($opac_default_sms_notify && $opac_default_sms_notify->value =~ /^"(.*)"$/) {
136 $$params{sms_notify} = $1;
142 # Remove oargs from params so holds can be created.
143 if ($$params{oargs}) {
144 delete $$params{oargs};
147 my $ahr = construct_hold_request_object($params);
148 my ($res2) = $self->method_lookup(
150 ? 'open-ils.circ.holds.create.override'
151 : 'open-ils.circ.holds.create'
152 )->run($auth, $ahr, $oargs);
154 'target' => $$params{$target_field},
157 $conn->respond($res2);
160 'target' => $$params{$target_field},
163 $conn->respond($res);
169 __PACKAGE__->register_method(
170 method => "test_and_create_batch_hold_event",
171 api_name => "open-ils.circ.holds.test_and_create.subscription_batch",
174 desc => q/This is for batch creating a set of holds where every field is identical except for the target users./,
176 { desc => 'Authentication token', type => 'string' },
177 { desc => 'Hash of named parameters. Same as for open-ils.circ.title_hold.is_possible, though the pertinent target field is automatically populated based on the hold_type and the specified list of target users.', type => 'object'},
178 { desc => 'Container ID of the user bucket holding target users', type => 'number' },
179 { desc => 'target object ID (clarified by hold_type in param hash)', type => 'number' },
180 { desc => 'Randomize flag, set to 0 for "not randomized"', type => 'bool' }
183 desc => 'Stream of objects structured as {total=>X, count=>Y, target=>Z, patronid=>A, result=>$hold_id} on success, -1 on missing arg, event (or stream of events on "result" key of object) on error(s)',
188 __PACKAGE__->register_method(
189 method => "test_and_create_batch_hold_event",
190 api_name => "open-ils.circ.holds.test_and_create.subscription_batch.override",
193 desc => '@see open-ils.circ.holds.test_and_create.subscription_batch',
198 sub test_and_create_batch_hold_event {
199 my( $self, $conn, $auth, $params, $target_bucket, $target_id, $randomize, $oargs ) = @_;
202 $randomize //= 1; # default to random hold creation order
203 $$params{hold_type} //= 'T'; # default to title holds
206 if ($self->api_name =~ /override/) {
208 $oargs = { all => 1 } unless defined $oargs;
209 $$params{oargs} = $oargs; # for is_possible checking.
212 my $e = new_editor(authtoken=>$auth);
213 return $e->die_event unless $e->checkauth;
214 $$params{'requestor'} = $e->requestor->id;
217 my $org = $e->requestor->ws_ou || $e->requestor->home_ou;
218 # the perm locaiton shouldn't really matter here since holds
219 # will exist all over and MANAGE_HOLD_GROUPS should be universal
220 my $evt = $U->check_perms($e->requestor->id, $org, 'MANAGE_HOLD_GROUPS');
223 my $rand_setting = $U->ou_ancestor_setting_value($org, 'holds.subscription.randomize');
224 $randomize = $rand_setting if (defined $rand_setting);
227 if ($$params{'hold_type'} eq 'T') { $target_field = 'titleid'; }
228 elsif ($$params{'hold_type'} eq 'C') { $target_field = 'copy_id'; }
229 elsif ($$params{'hold_type'} eq 'R') { $target_field = 'copy_id'; }
230 elsif ($$params{'hold_type'} eq 'F') { $target_field = 'copy_id'; }
231 elsif ($$params{'hold_type'} eq 'I') { $target_field = 'issuanceid'; }
232 elsif ($$params{'hold_type'} eq 'V') { $target_field = 'volume_id'; }
233 elsif ($$params{'hold_type'} eq 'M') { $target_field = 'mrid'; }
234 elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
235 else { return undef; }
237 # Check for a valid record.
238 # XXX For now, because only title holds are allowed, we'll add only that check.
239 my $target_check = $e->json_query({
240 select => {bre => ['id']},
242 where => {deleted => 'f', id => $target_id}
244 return {error=>'invalid_target'} if (!@$target_check);
246 my $formats_map = delete($$params{holdable_formats_map}) || {};
248 my $target_list = $e->search_container_user_bucket_item({bucket => $target_bucket});
249 @$target_list = shuffle(@$target_list) if $randomize;
251 # Record the request...
253 my $bhe = Fieldmapper::action::batch_hold_event->new;
255 $bhe->staff($e->requestor->id);
256 $bhe->bucket($target_bucket);
257 $bhe->target($target_id);
258 $bhe->hold_type($$params{hold_type});
259 $bhe = $e->create_action_batch_hold_event($bhe) or return $e->die_event;
262 my $total = scalar(@$target_list);
264 $conn->respond({total => $total, count => $count});
266 my $triggers = OpenSRF::AppSession->connect('open-ils.trigger');
267 foreach (@$target_list) {
269 $$params{$target_field} = $target_id;
270 $$params{patronid} = $_->target_user;
272 my $usr = $e->retrieve_actor_user([
281 my $user_setting_map = {
282 map { $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) }
286 $$params{pickup_lib} = $$user_setting_map{'opac.default_pickup_location'} || $usr->home_ou;
288 if ($user_setting_map->{'opac.hold_notify'} =~ /email/) {
289 $$params{email_notify} = 1;
291 delete $$params{email_notify};
294 if ($user_setting_map->{'opac.default_phone'} && $user_setting_map->{'opac.hold_notify'} =~ /phone/) {
295 $$params{phone_notify} = $user_setting_map->{'opac.default_phone'};
297 delete $$params{phone_notify};
300 if ($user_setting_map->{'opac.default_sms_carrier'}
301 && $user_setting_map->{'opac.default_sms_notify'}
302 && $user_setting_map->{'opac.hold_notify'} =~ /sms/) {
303 $$params{sms_carrier} = $user_setting_map->{'opac.default_sms_carrier'};
304 $$params{sms_notify} = $user_setting_map->{'opac.default_sms_notify'};
306 delete $$params{sms_carrier};
307 delete $$params{sms_notify};
310 # copy the requested formats from the target->formats map
311 # into the top-level formats attr for each hold ... empty for now, T holds only
312 $$params{holdable_formats} = $formats_map->{$_};
315 ($res) = $self->method_lookup(
316 'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
317 if ($res->{'success'} == 1) {
319 $params->{'depth'} = $res->{'depth'} if $res->{'depth'};
321 # Remove oargs from params so holds can be created.
322 if ($$params{oargs}) {
323 delete $$params{oargs};
326 my $ahr = construct_hold_request_object($params);
327 my ($res2) = $self->method_lookup(
329 ? 'open-ils.circ.holds.create.override'
330 : 'open-ils.circ.holds.create'
331 )->run($auth, $ahr, $oargs);
333 total => $total, count => $count,
334 'patronid' => $$params{patronid},
335 'target' => $$params{$target_field},
338 $conn->respond($res2);
340 unless (ref($res2->{result})) { # success returns a hold id only
342 my $bhem = Fieldmapper::action::batch_hold_event_map->new;
344 $bhem->batch_hold_event($bhe->id);
345 $bhem->hold($res2->{result});
346 $e->create_action_batch_hold_event_map($bhem) or return $e->die_event;
349 my $hold = $e->retrieve_action_hold_request($bhem->hold);
350 $triggers->request('open-ils.trigger.event.autocreate', 'hold_request.success', $hold, $hold->pickup_lib);
355 total => $total, count => $count,
356 'target' => $$params{$target_field},
357 'failedpatronid' => $$params{patronid},
360 $conn->respond($res);
367 __PACKAGE__->register_method(
368 method => "rollback_batch_hold_event",
369 api_name => "open-ils.circ.holds.rollback.subscription_batch",
372 desc => q/This is for batch creating a set of holds where every field is identical except for the target users./,
374 { desc => 'Authentication token', type => 'string' },
375 { desc => 'Hold Group Event ID to roll back', type => 'number' },
378 desc => 'Stream of objects structured as {total=>X, count=>Y} on success, event on error',
383 sub rollback_batch_hold_event {
384 my( $self, $conn, $auth, $event_id ) = @_;
386 my $e = new_editor(authtoken=>$auth,xact=>1);
387 return $e->die_event unless $e->checkauth;
389 my $org = $e->requestor->ws_ou || $e->requestor->home_ou;
390 my $evt = $U->check_perms($e->requestor->id, $org, 'MANAGE_HOLD_GROUPS');
393 my $batch_event = $e->retrieve_action_batch_hold_event($event_id);
394 my $target_list = $e->search_action_batch_hold_event_map({batch_hold_event => $event_id});
396 my $total = scalar(@$target_list);
398 $conn->respond({total => $total, count => $count});
400 for my $target (@$target_list) {
402 $self->method_lookup('open-ils.circ.hold.cancel')->run($auth, $target->hold, 8);
403 $conn->respond({ total => $total, count => $count });
406 $batch_event->cancelled('now');
407 $e->update_action_batch_hold_event($batch_event);
412 sub construct_hold_request_object {
415 my $ahr = Fieldmapper::action::hold_request->new;
418 foreach my $field (keys %{ $params }) {
419 if ($field eq 'depth') { $ahr->selection_depth($$params{$field}); }
420 elsif ($field eq 'patronid') {
421 $ahr->usr($$params{$field}); }
422 elsif ($field eq 'titleid') { $ahr->target($$params{$field}); }
423 elsif ($field eq 'copy_id') { $ahr->target($$params{$field}); }
424 elsif ($field eq 'issuanceid') { $ahr->target($$params{$field}); }
425 elsif ($field eq 'volume_id') { $ahr->target($$params{$field}); }
426 elsif ($field eq 'mrid') { $ahr->target($$params{$field}); }
427 elsif ($field eq 'partid') { $ahr->target($$params{$field}); }
429 $ahr->$field($$params{$field});
435 __PACKAGE__->register_method(
436 method => "create_hold_batch",
437 api_name => "open-ils.circ.holds.create.batch",
440 desc => q/@see open-ils.circ.holds.create.batch/,
442 { desc => 'Authentication token', type => 'string' },
443 { desc => 'Array of hold objects', type => 'array' }
446 desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
451 __PACKAGE__->register_method(
452 method => "create_hold_batch",
453 api_name => "open-ils.circ.holds.create.override.batch",
456 desc => '@see open-ils.circ.holds.create.batch',
461 sub create_hold_batch {
462 my( $self, $conn, $auth, $hold_list, $oargs ) = @_;
463 (my $method = $self->api_name) =~ s/\.batch//og;
464 foreach (@$hold_list) {
465 my ($res) = $self->method_lookup($method)->run($auth, $_, $oargs);
466 $conn->respond($res);
472 __PACKAGE__->register_method(
473 method => "create_hold",
474 api_name => "open-ils.circ.holds.create",
476 desc => "Create a new hold for an item. From a permissions perspective, " .
477 "the login session is used as the 'requestor' of the hold. " .
478 "The hold recipient is determined by the 'usr' setting within the hold object. " .
479 'First we verify the requestor has holds request permissions. ' .
480 'Then we verify that the recipient is allowed to make the given hold. ' .
481 'If not, we see if the requestor has "override" capabilities. If not, ' .
482 'a permission exception is returned. If permissions allow, we cycle ' .
483 'through the set of holds objects and create. ' .
484 'If the recipient does not have permission to place multiple holds ' .
485 'on a single title and said operation is attempted, a permission ' .
486 'exception is returned',
488 { desc => 'Authentication token', type => 'string' },
489 { desc => 'Hold object for hold to be created',
490 type => 'object', class => 'ahr' }
493 desc => 'New ahr ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
498 __PACKAGE__->register_method(
499 method => "create_hold",
500 api_name => "open-ils.circ.holds.create.override",
501 notes => '@see open-ils.circ.holds.create',
503 desc => "If the recipient is not allowed to receive the requested hold, " .
504 "call this method to attempt the override",
506 { desc => 'Authentication token', type => 'string' },
508 desc => 'Hold object for hold to be created',
509 type => 'object', class => 'ahr'
513 desc => 'New hold (ahr) ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
519 my( $self, $conn, $auth, $hold, $oargs ) = @_;
520 return -1 unless $hold;
521 my $e = new_editor(authtoken=>$auth, xact=>1);
522 return $e->die_event unless $e->checkauth;
525 if ($self->api_name =~ /override/) {
527 $oargs = { all => 1 } unless defined $oargs;
532 my $requestor = $e->requestor;
533 my $recipient = $requestor;
535 if( $requestor->id ne $hold->usr ) {
536 # Make sure the requestor is allowed to place holds for
537 # the recipient if they are not the same people
538 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->die_event;
539 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->die_event;
542 # If the related org setting tells us to, block if patron privs have expired
543 my $expire_setting = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_BLOCK_HOLD_FOR_EXPIRED_PATRON);
544 if ($expire_setting) {
545 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
546 clean_ISO8601($recipient->expire_date));
548 push( @events, OpenILS::Event->new(
549 'PATRON_ACCOUNT_EXPIRED',
550 "payload" => {"fail_part" => "actor.usr.privs_expired"}
551 )) if( CORE::time > $expire->epoch ) ;
554 # Now make sure the recipient is allowed to receive the specified hold
555 my $porg = $recipient->home_ou;
556 my $rid = $e->requestor->id;
557 my $t = $hold->hold_type;
559 # See if a duplicate hold already exists
561 usr => $recipient->id,
563 fulfillment_time => undef,
564 target => $hold->target,
565 cancel_time => undef,
568 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
570 my $existing = $e->search_action_hold_request($sargs);
572 # See if the requestor has the CREATE_DUPLICATE_HOLDS perm.
573 my $can_dup = $e->allowed('CREATE_DUPLICATE_HOLDS', $recipient->home_ou);
574 # How many are allowed.
575 my $num_dups = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_MAX_DUPLICATE_HOLDS, $e) || 0;
576 push( @events, OpenILS::Event->new('HOLD_EXISTS'))
577 unless (($t eq 'T' || $t eq 'M') && $can_dup && scalar(@$existing) < $num_dups);
578 # Note: We check for @$existing < $num_dups because we're adding a hold with this call.
581 my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
582 push( @events, OpenILS::Event->new('HOLD_ITEM_CHECKED_OUT')) if $checked_out;
584 if ( $t eq OILS_HOLD_TYPE_METARECORD ) {
585 return $e->die_event unless $e->allowed('MR_HOLDS', $porg);
586 } elsif ( $t eq OILS_HOLD_TYPE_TITLE ) {
587 return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
588 } elsif ( $t eq OILS_HOLD_TYPE_VOLUME ) {
589 return $e->die_event unless $e->allowed('VOLUME_HOLDS', $porg);
590 } elsif ( $t eq OILS_HOLD_TYPE_MONOPART ) {
591 return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
592 } elsif ( $t eq OILS_HOLD_TYPE_ISSUANCE ) {
593 return $e->die_event unless $e->allowed('ISSUANCE_HOLDS', $porg);
594 } elsif ( $t eq OILS_HOLD_TYPE_COPY ) {
595 return $e->die_event unless $e->allowed('COPY_HOLDS', $porg);
596 } elsif ( $t eq OILS_HOLD_TYPE_FORCE || $t eq OILS_HOLD_TYPE_RECALL ) {
597 my $copy = $e->retrieve_asset_copy($hold->target)
598 or return $e->die_event;
599 if ( $t eq OILS_HOLD_TYPE_FORCE ) {
600 return $e->die_event unless $e->allowed('COPY_HOLDS_FORCE', $copy->circ_lib);
601 } elsif ( $t eq OILS_HOLD_TYPE_RECALL ) {
602 return $e->die_event unless $e->allowed('COPY_HOLDS_RECALL', $copy->circ_lib);
611 for my $evt (@events) {
613 my $name = $evt->{textcode};
614 if($oargs->{all} || grep { $_ eq $name } @{$oargs->{events}}) {
615 return $e->die_event unless $e->allowed("$name.override", $porg);
623 # Check for hold expiration in the past, and set it to empty string.
624 $hold->expire_time(undef) if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1);
626 # set the configured expire time
627 unless($hold->expire_time || $U->is_true($hold->frozen)) {
628 $hold->expire_time(calculate_expire_time($recipient->home_ou));
632 # if behind-the-desk pickup is supported at the hold pickup lib,
633 # set the value to the patron default, unless a value has already
634 # been applied. If it's not supported, force the value to false.
636 my $bdous = $U->ou_ancestor_setting_value(
638 'circ.holds.behind_desk_pickup_supported', $e);
641 if (!defined $hold->behind_desk) {
643 my $set = $e->search_actor_user_setting({
645 name => 'circ.holds_behind_desk'
648 $hold->behind_desk('t') if $set and
649 OpenSRF::Utils::JSON->JSON2perl($set->value);
652 # behind the desk not supported, force it to false
653 $hold->behind_desk('f');
656 $hold->requestor($e->requestor->id);
657 $hold->request_lib($e->requestor->ws_ou);
658 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
659 $hold = $e->create_action_hold_request($hold) or return $e->die_event;
663 $conn->respond_complete($hold->id);
665 $U->simplereq('open-ils.hold-targeter',
666 'open-ils.hold-targeter.target', {hold => $hold->id}
667 ) unless $U->is_true($hold->frozen);
672 # makes sure that a user has permission to place the type of requested hold
673 # returns the Perm exception if not allowed, returns undef if all is well
674 sub _check_holds_perm {
675 my($type, $user_id, $org_id) = @_;
679 $evt = $apputils->check_perms($user_id, $org_id, "MR_HOLDS" );
680 } elsif ($type eq "T") {
681 $evt = $apputils->check_perms($user_id, $org_id, "TITLE_HOLDS" );
682 } elsif($type eq "V") {
683 $evt = $apputils->check_perms($user_id, $org_id, "VOLUME_HOLDS");
684 } elsif($type eq "C") {
685 $evt = $apputils->check_perms($user_id, $org_id, "COPY_HOLDS" );
692 # tests if the given user is allowed to place holds on another's behalf
693 sub _check_request_holds_perm {
696 if (my $evt = $apputils->check_perms(
697 $user_id, $org_id, "REQUEST_HOLDS")) {
702 my $ses_is_req_note = 'The login session is the requestor. If the requestor is different from the user, ' .
703 'then the requestor must have VIEW_HOLD permissions';
705 __PACKAGE__->register_method(
706 method => "retrieve_holds_by_id",
707 api_name => "open-ils.circ.holds.retrieve_by_id",
709 desc => "Retrieve the hold, with hold transits attached, for the specified ID. $ses_is_req_note",
711 { desc => 'Authentication token', type => 'string' },
712 { desc => 'Hold ID', type => 'number' }
715 desc => 'Hold object with transits attached, event on error',
721 sub retrieve_holds_by_id {
722 my($self, $client, $auth, $hold_id) = @_;
723 my $e = new_editor(authtoken=>$auth);
724 $e->checkauth or return $e->event;
725 $e->allowed('VIEW_HOLD') or return $e->event;
727 my $holds = $e->search_action_hold_request(
729 { id => $hold_id , fulfillment_time => undef },
731 order_by => { ahr => "request_time" },
733 flesh_fields => {ahr => ['notes']}
738 flesh_hold_transits($holds);
739 flesh_hold_notices($holds, $e);
744 __PACKAGE__->register_method(
745 method => "retrieve_holds",
746 api_name => "open-ils.circ.holds.retrieve",
748 desc => "Retrieves all the holds, with hold transits attached, for the specified user. $ses_is_req_note",
750 { desc => 'Authentication token', type => 'string' },
751 { desc => 'User ID', type => 'integer' },
752 { desc => 'Available Only', type => 'boolean' }
755 desc => 'list of holds, event on error',
760 __PACKAGE__->register_method(
761 method => "retrieve_holds",
762 api_name => "open-ils.circ.holds.id_list.retrieve",
765 desc => "Retrieves all the hold IDs, for the specified user. $ses_is_req_note",
767 { desc => 'Authentication token', type => 'string' },
768 { desc => 'User ID', type => 'integer' },
769 { desc => 'Available Only', type => 'boolean' }
772 desc => 'list of holds, event on error',
777 __PACKAGE__->register_method(
778 method => "retrieve_holds",
779 api_name => "open-ils.circ.holds.canceled.retrieve",
782 desc => "Retrieves all the cancelled holds for the specified user. $ses_is_req_note",
784 { desc => 'Authentication token', type => 'string' },
785 { desc => 'User ID', type => 'integer' }
788 desc => 'list of holds, event on error',
793 __PACKAGE__->register_method(
794 method => "retrieve_holds",
795 api_name => "open-ils.circ.holds.canceled.id_list.retrieve",
798 desc => "Retrieves list of cancelled hold IDs for the specified user. $ses_is_req_note",
800 { desc => 'Authentication token', type => 'string' },
801 { desc => 'User ID', type => 'integer' }
804 desc => 'list of hold IDs, event on error',
811 my ($self, $client, $auth, $user_id, $available) = @_;
813 my $e = new_editor(authtoken=>$auth);
814 return $e->event unless $e->checkauth;
815 $user_id = $e->requestor->id unless defined $user_id;
817 my $notes_filter = {staff => 'f'};
818 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
819 unless($user_id == $e->requestor->id) {
820 if($e->allowed('VIEW_HOLD', $user->home_ou)) {
821 $notes_filter = {staff => 't'}
823 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
824 $e, $user_id, $e->requestor->id, 'hold.view');
825 return $e->event unless $allowed;
828 # staff member looking at his/her own holds can see staff and non-staff notes
829 $notes_filter = {} if $e->allowed('VIEW_HOLD', $user->home_ou);
833 select => {ahr => ['id']},
835 where => {usr => $user_id, fulfillment_time => undef}
838 if($self->api_name =~ /canceled/) {
840 $holds_query->{order_by} =
841 [{class => 'ahr', field => 'cancel_time', direction => 'desc'}];
843 recently_canceled_holds_filter($e, $holds_query);
847 # order non-cancelled holds by ready-for-pickup, then active, followed by suspended
848 # "compare" sorts false values to the front. testing pickup_lib != current_shelf_lib
849 # will sort by pl = csl > pl != csl > followed by csl is null;
850 $holds_query->{order_by} = [
852 field => 'pickup_lib',
853 compare => {'!=' => {'+ahr' => 'current_shelf_lib'}}},
854 {class => 'ahr', field => 'shelf_time'},
855 {class => 'ahr', field => 'frozen'},
856 {class => 'ahr', field => 'request_time'}
859 $holds_query->{where}->{cancel_time} = undef;
861 $holds_query->{where}->{shelf_time} = {'!=' => undef};
863 $holds_query->{where}->{pickup_lib} = {'=' => {'+ahr' => 'current_shelf_lib'}};
867 my $hold_ids = $e->json_query($holds_query);
868 $hold_ids = [ map { $_->{id} } @$hold_ids ];
870 return $hold_ids if $self->api_name =~ /id_list/;
873 for my $hold_id ( @$hold_ids ) {
875 my $hold = $e->retrieve_action_hold_request($hold_id);
876 $hold->notes($e->search_action_hold_request_note({hold => $hold_id, %$notes_filter}));
879 $e->search_action_hold_transit_copy([
880 {hold => $hold->id, cancel_time => undef},
881 {order_by => {ahtc => 'source_send_time desc'}, limit => 1}])->[0]
891 # Creates / augments a set of query filters to search for canceled holds
892 # based on circ.holds.canceled.* org settings.
893 sub recently_canceled_holds_filter {
894 my ($e, $filters) = @_;
896 $filters->{where} ||= {};
899 my $cancel_count = $U->ou_ancestor_setting_value(
900 $e->requestor->ws_ou, 'circ.holds.canceled.display_count', $e);
902 unless($cancel_count) {
903 $cancel_age = $U->ou_ancestor_setting_value(
904 $e->requestor->ws_ou, 'circ.holds.canceled.display_age', $e);
906 # if no settings are defined, default to last 10 cancelled holds
907 $cancel_count = 10 unless $cancel_age;
910 if($cancel_count) { # limit by count
912 $filters->{where}->{cancel_time} = {'!=' => undef};
913 $filters->{limit} = $cancel_count;
915 } elsif($cancel_age) { # limit by age
917 # find all of the canceled holds that were canceled within the configured time frame
918 my $date = DateTime->now->subtract(seconds =>
919 OpenILS::Utils::DateTime->interval_to_seconds($cancel_age));
921 $date = $U->epoch2ISO8601($date->epoch);
922 $filters->{where}->{cancel_time} = {'>=' => $date};
930 __PACKAGE__->register_method(
931 method => 'user_hold_count',
932 api_name => 'open-ils.circ.hold.user.count'
935 sub user_hold_count {
936 my ( $self, $conn, $auth, $userid ) = @_;
937 my $e = new_editor( authtoken => $auth );
938 return $e->event unless $e->checkauth;
939 my $patron = $e->retrieve_actor_user($userid)
941 return $e->event unless $e->allowed( 'VIEW_HOLD', $patron->home_ou );
942 return __user_hold_count( $self, $e, $userid );
945 sub __user_hold_count {
946 my ( $self, $e, $userid ) = @_;
947 my $holds = $e->search_action_hold_request(
950 fulfillment_time => undef,
951 cancel_time => undef,
956 return scalar(@$holds);
960 __PACKAGE__->register_method(
961 method => "retrieve_holds_by_pickup_lib",
962 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
964 "Retrieves all the holds, with hold transits attached, for the specified pickup_ou id."
967 __PACKAGE__->register_method(
968 method => "retrieve_holds_by_pickup_lib",
969 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
970 notes => "Retrieves all the hold ids for the specified pickup_ou id. "
973 sub retrieve_holds_by_pickup_lib {
974 my ($self, $client, $login_session, $ou_id) = @_;
976 #FIXME -- put an appropriate permission check here
977 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
978 # $login_session, $user_id, 'VIEW_HOLD' );
979 #return $evt if $evt;
981 my $holds = $apputils->simplereq(
983 "open-ils.cstore.direct.action.hold_request.search.atomic",
985 pickup_lib => $ou_id ,
986 fulfillment_time => undef,
989 { order_by => { ahr => "request_time" } }
992 if ( ! $self->api_name =~ /id_list/ ) {
993 flesh_hold_transits($holds);
997 return [ map { $_->id } @$holds ];
1001 __PACKAGE__->register_method(
1002 method => "uncancel_hold",
1003 api_name => "open-ils.circ.hold.uncancel"
1007 my($self, $client, $auth, $hold_id) = @_;
1008 my $e = new_editor(authtoken=>$auth, xact=>1);
1009 return $e->die_event unless $e->checkauth;
1011 my $hold = $e->retrieve_action_hold_request($hold_id)
1012 or return $e->die_event;
1013 return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
1015 if ($hold->fulfillment_time) {
1019 unless ($hold->cancel_time) {
1024 # if configured to reset the request time, also reset the expire time
1025 if($U->ou_ancestor_setting_value(
1026 $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
1028 $hold->request_time('now');
1029 $hold->expire_time(calculate_expire_time($hold->request_lib));
1032 $hold->clear_cancel_time;
1033 $hold->clear_cancel_cause;
1034 $hold->clear_cancel_note;
1035 $hold->clear_shelf_time;
1036 $hold->clear_current_copy;
1037 $hold->clear_capture_time;
1038 $hold->clear_prev_check_time;
1039 $hold->clear_shelf_expire_time;
1040 $hold->clear_current_shelf_lib;
1042 $e->update_action_hold_request($hold) or return $e->die_event;
1045 $U->simplereq('open-ils.hold-targeter',
1046 'open-ils.hold-targeter.target', {hold => $hold_id});
1052 __PACKAGE__->register_method(
1053 method => "cancel_hold",
1054 api_name => "open-ils.circ.hold.cancel",
1056 desc => 'Cancels the specified hold. The login session is the requestor. If the requestor is different from the usr field ' .
1057 'on the hold, the requestor must have CANCEL_HOLDS permissions. The hold may be either the hold object or the hold id',
1059 {desc => 'Authentication token', type => 'string'},
1060 {desc => 'Hold ID', type => 'number'},
1061 {desc => 'Cause of Cancellation', type => 'string'},
1062 {desc => 'Note', type => 'string'}
1065 desc => '1 on success, event on error'
1071 my($self, $client, $auth, $holdid, $cause, $note) = @_;
1073 my $e = new_editor(authtoken=>$auth, xact=>1);
1074 return $e->die_event unless $e->checkauth;
1076 my $hold = $e->retrieve_action_hold_request($holdid)
1077 or return $e->die_event;
1079 if( $e->requestor->id ne $hold->usr ) {
1080 return $e->die_event unless $e->allowed('CANCEL_HOLDS');
1083 if ($hold->cancel_time) {
1088 # If the hold is captured, reset the copy status
1089 if( $hold->capture_time and $hold->current_copy ) {
1091 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1092 or return $e->die_event;
1094 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1095 $logger->info("canceling hold $holdid whose item is on the holds shelf");
1096 # $logger->info("setting copy to status 'reshelving' on hold cancel");
1097 # $copy->status(OILS_COPY_STATUS_RESHELVING);
1098 # $copy->editor($e->requestor->id);
1099 # $copy->edit_date('now');
1100 # $e->update_asset_copy($copy) or return $e->event;
1102 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1104 my $hid = $hold->id;
1105 $logger->warn("! canceling hold [$hid] that is in transit");
1106 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
1109 my $trans = $e->retrieve_action_transit_copy($transid);
1110 # Leave the transit alive, but set the copy status to
1111 # reshelving so it will be properly reshelved when it gets back home
1113 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
1114 $e->update_action_transit_copy($trans) or return $e->die_event;
1120 $hold->cancel_time('now');
1121 $hold->cancel_cause($cause);
1122 $hold->cancel_note($note);
1123 $e->update_action_hold_request($hold)
1124 or return $e->die_event;
1128 # re-fetch the hold to pick up the real cancel_time (not "now") for A/T
1130 $hold = $e->retrieve_action_hold_request($hold->id) or return $e->die_event;
1133 if ($e->requestor->id == $hold->usr) {
1134 $U->create_events_for_hook('hold_request.cancel.patron', $hold, $hold->pickup_lib);
1136 $U->create_events_for_hook('hold_request.cancel.staff', $hold, $hold->pickup_lib);
1142 my $update_hold_desc = 'The login session is the requestor. ' .
1143 'If the requestor is different from the usr field on the hold, ' .
1144 'the requestor must have UPDATE_HOLDS permissions. ' .
1145 'If supplying a hash of hold data, "id" must be included. ' .
1146 'The hash is ignored if a hold object is supplied, ' .
1147 'so you should supply only one kind of hold data argument.' ;
1149 __PACKAGE__->register_method(
1150 method => "update_hold",
1151 api_name => "open-ils.circ.hold.update",
1153 desc => "Updates the specified hold. $update_hold_desc",
1155 {desc => 'Authentication token', type => 'string'},
1156 {desc => 'Hold Object', type => 'object'},
1157 {desc => 'Hash of values to be applied', type => 'object'}
1160 desc => 'Hold ID on success, event on error',
1166 __PACKAGE__->register_method(
1167 method => "batch_update_hold",
1168 api_name => "open-ils.circ.hold.update.batch",
1171 desc => "Updates the specified hold(s). $update_hold_desc",
1173 {desc => 'Authentication token', type => 'string'},
1174 {desc => 'Array of hold obejcts', type => 'array' },
1175 {desc => 'Array of hashes of values to be applied', type => 'array' }
1178 desc => 'Hold ID per success, event per error',
1184 my($self, $client, $auth, $hold, $values) = @_;
1185 my $e = new_editor(authtoken=>$auth, xact=>1);
1186 return $e->die_event unless $e->checkauth;
1187 my $resp = update_hold_impl($self, $e, $hold, $values);
1188 if ($U->event_code($resp)) {
1192 $e->commit; # FIXME: update_hold_impl already does $e->commit ??
1196 sub batch_update_hold {
1197 my($self, $client, $auth, $hold_list, $values_list) = @_;
1198 my $e = new_editor(authtoken=>$auth);
1199 return $e->die_event unless $e->checkauth;
1201 my $count = ($hold_list) ? scalar(@$hold_list) : scalar(@$values_list); # FIXME: we don't know for sure that we got $values_list. we could have neither list.
1203 $values_list ||= []; # FIXME: either move this above $count declaration, or send an event if both lists undef. Probably the latter.
1205 # FIXME: Failing over to [] guarantees warnings for "Use of unitialized value" in update_hold_impl call.
1206 # FIXME: We should be sure we only call update_hold_impl with hold object OR hash, not both.
1208 for my $idx (0..$count-1) {
1210 my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
1211 $e->xact_commit unless $U->event_code($resp);
1212 $client->respond($resp);
1216 return undef; # not in the register return type, assuming we should always have at least one list populated
1219 sub update_hold_impl {
1220 my($self, $e, $hold, $values) = @_;
1222 my $need_retarget = 0;
1225 $hold = $e->retrieve_action_hold_request($values->{id})
1226 or return $e->die_event;
1227 for my $k (keys %$values) {
1228 # Outside of pickup_lib (covered by the first regex) I don't know when these would currently change.
1229 # But hey, why not cover things that may happen later?
1230 if ($k =~ '_(lib|ou)$' || $k eq 'target' || $k eq 'hold_type' || $k eq 'requestor' || $k eq 'selection_depth' || $k eq 'holdable_formats') {
1231 if (defined $values->{$k} && defined $hold->$k() && $values->{$k} ne $hold->$k()) {
1232 # Value changed? RETARGET!
1234 } elsif (defined $hold->$k() != defined $values->{$k}) {
1235 # Value being set or cleared? RETARGET!
1239 if (defined $values->{$k}) {
1240 $hold->$k($values->{$k});
1242 my $f = "clear_$k"; $hold->$f();
1247 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
1248 or return $e->die_event;
1250 # don't allow the user to be changed
1251 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
1253 if($hold->usr ne $e->requestor->id) {
1254 # if the hold is for a different user, make sure the
1255 # requestor has the appropriate permissions
1256 my $usr = $e->retrieve_actor_user($hold->usr)
1257 or return $e->die_event;
1258 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1262 # --------------------------------------------------------------
1263 # Changing the request time is like playing God
1264 # --------------------------------------------------------------
1265 if($hold->request_time ne $orig_hold->request_time) {
1266 return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
1267 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
1271 # --------------------------------------------------------------
1272 # Code for making sure staff have appropriate permissons for cut_in_line
1273 # This, as is, doesn't prevent a user from cutting their own holds in line
1275 # --------------------------------------------------------------
1276 if($U->is_true($hold->cut_in_line) ne $U->is_true($orig_hold->cut_in_line)) {
1277 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
1281 # --------------------------------------------------------------
1282 # Disallow hold suspencion if the hold is already captured.
1283 # --------------------------------------------------------------
1284 if ($U->is_true($hold->frozen) and not $U->is_true($orig_hold->frozen)) {
1285 $hold_status = _hold_status($e, $hold);
1286 if ($hold_status > 2 && $hold_status != 7) { # hold is captured
1287 $logger->info("bypassing hold freeze on captured hold");
1288 return OpenILS::Event->new('HOLD_SUSPEND_AFTER_CAPTURE');
1293 # --------------------------------------------------------------
1294 # if the hold is on the holds shelf or in transit and the pickup
1295 # lib changes we need to create a new transit.
1296 # --------------------------------------------------------------
1297 if($orig_hold->pickup_lib ne $hold->pickup_lib) {
1299 $hold_status = _hold_status($e, $hold) unless $hold_status;
1301 if($hold_status == 3) { # in transit
1303 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
1304 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
1306 $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
1308 # update the transit to reflect the new pickup location
1309 my $transit = $e->search_action_hold_transit_copy(
1310 {hold=>$hold->id, cancel_time => undef, dest_recv_time => undef})->[0]
1311 or return $e->die_event;
1313 $transit->prev_dest($transit->dest); # mark the previous destination on the transit
1314 $transit->dest($hold->pickup_lib);
1315 $e->update_action_hold_transit_copy($transit) or return $e->die_event;
1317 } elsif($hold_status == 4 or $hold_status == 5 or $hold_status == 8) { # on holds shelf
1319 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
1320 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
1322 $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
1324 if ($hold->pickup_lib eq $orig_hold->current_shelf_lib) {
1325 # This can happen if the pickup lib is changed while the hold is
1326 # on the shelf, then changed back to the original pickup lib.
1327 # Restore the original shelf_expire_time to prevent abuse.
1328 set_hold_shelf_expire_time(undef, $hold, $e, $hold->shelf_time);
1331 # clear to prevent premature shelf expiration
1332 $hold->clear_shelf_expire_time;
1334 # If a copy is targeted and pickup lib changes,
1335 # clear the current_copy so a retarget will re-evaluate
1336 # the hold from scratch.
1337 } elsif ($hold_status == 2) {
1338 $logger->info("Pickup location changed and waiting for capture, clear current_copy for hold ".$hold->id);
1339 $hold->clear_current_copy;
1343 if($U->is_true($hold->frozen)) {
1344 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1345 $hold->clear_current_copy;
1346 $hold->clear_prev_check_time;
1347 # Clear expire_time to prevent frozen holds from expiring.
1348 $logger->info("clearing expire_time for frozen hold ".$hold->id);
1349 $hold->clear_expire_time;
1352 # If the hold_expire_time is in the past && is not equal to the
1353 # original expire_time, then reset the expire time to be in the
1355 if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1 && $U->datecmp($hold->expire_time, $orig_hold->expire_time) != 0) {
1356 $hold->expire_time(calculate_expire_time($hold->request_lib));
1359 # If the hold is reactivated, reset the expire_time.
1360 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1361 $logger->info("Reset expire_time on activated hold ".$hold->id);
1362 $hold->expire_time(calculate_expire_time($hold->request_lib));
1365 $e->update_action_hold_request($hold) or return $e->die_event;
1368 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1369 $logger->info("Running targeter on activated hold ".$hold->id);
1370 $U->simplereq('open-ils.hold-targeter',
1371 'open-ils.hold-targeter.target', {hold => $hold->id});
1374 # a change to mint-condition changes the set of potential copies, so retarget the hold;
1375 if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
1376 _reset_hold($self, $e->requestor, $hold)
1377 } elsif($need_retarget && !defined $hold->capture_time()) { # If needed, retarget the hold due to changes
1378 $U->simplereq('open-ils.hold-targeter',
1379 'open-ils.hold-targeter.target', {hold => $hold->id});
1385 # this does not update the hold in the DB. It only
1386 # sets the shelf_expire_time field on the hold object.
1387 # start_time is optional and defaults to 'now'
1388 sub set_hold_shelf_expire_time {
1389 my ($class, $hold, $editor, $start_time) = @_;
1391 my $shelf_expire = $U->ou_ancestor_setting_value(
1393 'circ.holds.default_shelf_expire_interval',
1397 return undef unless $shelf_expire;
1399 $start_time = ($start_time) ?
1400 DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time)) :
1401 DateTime->now(time_zone => 'local'); # without time_zone we get UTC ... yuck!
1403 my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($shelf_expire);
1404 my $expire_time = $start_time->add(seconds => $seconds);
1406 # if the shelf expire time overlaps with a pickup lib's
1407 # closed date, push it out to the first open date
1408 my $dateinfo = $U->storagereq(
1409 'open-ils.storage.actor.org_unit.closed_date.overlap',
1410 $hold->pickup_lib, $expire_time->strftime('%FT%T%z'));
1413 my $dt_parser = DateTime::Format::ISO8601->new;
1414 $expire_time = $dt_parser->parse_datetime(clean_ISO8601($dateinfo->{end}));
1416 # TODO: enable/disable time bump via setting?
1417 $expire_time->set(hour => '23', minute => '59', second => '59');
1419 $logger->info("circulator: shelf_expire_time overlaps".
1420 " with closed date, pushing expire time to $expire_time");
1423 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
1429 my($e, $orig_hold, $hold, $copy) = @_;
1430 my $src = $orig_hold->pickup_lib;
1431 my $dest = $hold->pickup_lib;
1433 $logger->info("putting hold into transit on pickup_lib update");
1435 my $transit = Fieldmapper::action::hold_transit_copy->new;
1436 $transit->hold($hold->id);
1437 $transit->source($src);
1438 $transit->dest($dest);
1439 $transit->target_copy($copy->id);
1440 $transit->source_send_time('now');
1441 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1443 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1444 $copy->editor($e->requestor->id);
1445 $copy->edit_date('now');
1447 $e->create_action_hold_transit_copy($transit) or return $e->die_event;
1448 $e->update_asset_copy($copy) or return $e->die_event;
1452 # if the hold is frozen, this method ensures that the hold is not "targeted",
1453 # that is, it clears the current_copy and prev_check_time to essentiallly
1454 # reset the hold. If it is being activated, it runs the targeter in the background
1455 sub update_hold_if_frozen {
1456 my($self, $e, $hold, $orig_hold) = @_;
1457 return if $hold->capture_time;
1459 if($U->is_true($hold->frozen)) {
1460 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1461 $hold->clear_current_copy;
1462 $hold->clear_prev_check_time;
1465 if($U->is_true($orig_hold->frozen)) {
1466 $logger->info("Running targeter on activated hold ".$hold->id);
1467 $U->simplereq('open-ils.hold-targeter',
1468 'open-ils.hold-targeter.target', {hold => $hold->id});
1473 __PACKAGE__->register_method(
1474 method => "hold_note_CUD",
1475 api_name => "open-ils.circ.hold_request.note.cud",
1477 desc => 'Create, update or delete a hold request note. If the operator (from Auth. token) '
1478 . 'is not the owner of the hold, the UPDATE_HOLD permission is required',
1480 { desc => 'Authentication token', type => 'string' },
1481 { desc => 'Hold note object', type => 'object' }
1484 desc => 'Returns the note ID, event on error'
1490 my($self, $conn, $auth, $note) = @_;
1492 my $e = new_editor(authtoken => $auth, xact => 1);
1493 return $e->die_event unless $e->checkauth;
1495 my $hold = $e->retrieve_action_hold_request($note->hold)
1496 or return $e->die_event;
1498 if($hold->usr ne $e->requestor->id) {
1499 my $usr = $e->retrieve_actor_user($hold->usr);
1500 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1501 $note->staff('t') if $note->isnew;
1505 $e->create_action_hold_request_note($note) or return $e->die_event;
1506 } elsif($note->ischanged) {
1507 $e->update_action_hold_request_note($note) or return $e->die_event;
1508 } elsif($note->isdeleted) {
1509 $e->delete_action_hold_request_note($note) or return $e->die_event;
1517 __PACKAGE__->register_method(
1518 method => "retrieve_hold_status",
1519 api_name => "open-ils.circ.hold.status.retrieve",
1521 desc => 'Calculates the current status of the hold. The requestor must have ' .
1522 'VIEW_HOLD permissions if the hold is for a user other than the requestor' ,
1524 { desc => 'Hold ID', type => 'number' }
1527 # type => 'number', # event sometimes
1528 desc => <<'END_OF_DESC'
1529 Returns event on error or:
1530 -1 on error (for now),
1531 1 for 'waiting for copy to become available',
1532 2 for 'waiting for copy capture',
1535 5 for 'hold-shelf-delay'
1538 8 for 'captured, on wrong hold shelf'
1545 sub retrieve_hold_status {
1546 my($self, $client, $auth, $hold_id) = @_;
1548 my $e = new_editor(authtoken => $auth);
1549 return $e->event unless $e->checkauth;
1550 my $hold = $e->retrieve_action_hold_request($hold_id)
1551 or return $e->event;
1553 if( $e->requestor->id != $hold->usr ) {
1554 return $e->event unless $e->allowed('VIEW_HOLD');
1557 return _hold_status($e, $hold);
1563 if ($hold->cancel_time) {
1566 if ($U->is_true($hold->frozen) && !$hold->capture_time) {
1569 if ($hold->current_shelf_lib and $hold->current_shelf_lib ne $hold->pickup_lib) {
1572 if ($hold->fulfillment_time) {
1575 return 1 unless $hold->current_copy;
1576 return 2 unless $hold->capture_time;
1578 my $copy = $hold->current_copy;
1579 unless( ref $copy ) {
1580 $copy = $e->retrieve_asset_copy($hold->current_copy)
1581 or return $e->event;
1584 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
1586 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
1588 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
1589 return 4 unless $hs_wait_interval;
1591 # if a hold_shelf_status_delay interval is defined and start_time plus
1592 # the interval is greater than now, consider the hold to be in the virtual
1593 # "on its way to the holds shelf" status. Return 5.
1595 my $transit = $e->search_action_hold_transit_copy({
1597 target_copy => $copy->id,
1598 cancel_time => undef,
1599 dest_recv_time => {'!=' => undef},
1601 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
1602 $start_time = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time));
1603 my $end_time = $start_time->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($hs_wait_interval));
1605 return 5 if $end_time > DateTime->now;
1614 __PACKAGE__->register_method(
1615 method => "retrieve_hold_queue_stats",
1616 api_name => "open-ils.circ.hold.queue_stats.retrieve",
1618 desc => 'Returns summary data about the state of a hold',
1620 { desc => 'Authentication token', type => 'string'},
1621 { desc => 'Hold ID', type => 'number'},
1624 desc => q/Summary object with keys:
1625 total_holds : total holds in queue
1626 queue_position : current queue position
1627 potential_copies : number of potential copies for this hold
1628 estimated_wait : estimated wait time in days
1629 status : hold status
1630 -1 => error or unexpected state,
1631 1 => 'waiting for copy to become available',
1632 2 => 'waiting for copy capture',
1635 5 => 'hold-shelf-delay'
1642 sub retrieve_hold_queue_stats {
1643 my($self, $conn, $auth, $hold_id) = @_;
1644 my $e = new_editor(authtoken => $auth);
1645 return $e->event unless $e->checkauth;
1646 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
1647 if($e->requestor->id != $hold->usr) {
1648 return $e->event unless $e->allowed('VIEW_HOLD');
1650 return retrieve_hold_queue_status_impl($e, $hold);
1653 sub retrieve_hold_queue_status_impl {
1657 # The holds queue is defined as the distinct set of holds that share at
1658 # least one potential copy with the context hold, plus any holds that
1659 # share the same hold type and target. The latter part exists to
1660 # accomodate holds that currently have no potential copies
1661 my $q_holds = $e->json_query({
1663 # fetch cut_in_line and request_time since they're in the order_by
1664 # and we're asking for distinct values
1665 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1669 select => { ahcm => ['hold'] },
1674 'field' => 'target_copy',
1675 'fkey' => 'target_copy'
1679 where => { '+ahcm2' => { hold => $hold->id } },
1686 "field" => "cut_in_line",
1687 "transform" => "coalesce",
1689 "direction" => "desc"
1691 { "class" => "ahr", "field" => "request_time" }
1696 if (!@$q_holds) { # none? maybe we don't have a map ...
1697 $q_holds = $e->json_query({
1698 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1703 "field" => "cut_in_line",
1704 "transform" => "coalesce",
1706 "direction" => "desc"
1708 { "class" => "ahr", "field" => "request_time" }
1711 hold_type => $hold->hold_type,
1712 target => $hold->target,
1713 capture_time => undef,
1714 cancel_time => undef,
1716 {expire_time => undef },
1717 {expire_time => {'>' => 'now'}}
1725 for my $h (@$q_holds) {
1726 last if $h->{id} == $hold->id;
1730 my $hold_data = $e->json_query({
1732 acp => [ {column => 'id', transform => 'count', aggregate => 1, alias => 'count'} ],
1733 ccm => [ {column =>'avg_wait_time'} ]
1739 ccm => {type => 'left'}
1744 where => {'+ahcm' => {hold => $hold->id} }
1747 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1749 my $default_wait = $U->ou_ancestor_setting_value(
1750 $user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL, $e);
1751 my $min_wait = $U->ou_ancestor_setting_value(
1752 $user_org, 'circ.holds.min_estimated_wait_interval', $e);
1753 $min_wait = OpenILS::Utils::DateTime->interval_to_seconds($min_wait || '0 seconds');
1754 $default_wait ||= '0 seconds';
1756 # Estimated wait time is the average wait time across the set
1757 # of potential copies, divided by the number of potential copies
1758 # times the queue position.
1760 my $combined_secs = 0;
1761 my $num_potentials = 0;
1763 for my $wait_data (@$hold_data) {
1764 my $count += $wait_data->{count};
1765 $combined_secs += $count *
1766 OpenILS::Utils::DateTime->interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
1767 $num_potentials += $count;
1770 my $estimated_wait = -1;
1772 if($num_potentials) {
1773 my $avg_wait = $combined_secs / $num_potentials;
1774 $estimated_wait = $qpos * ($avg_wait / $num_potentials);
1775 $estimated_wait = $min_wait if $estimated_wait < $min_wait and $estimated_wait != -1;
1779 total_holds => scalar(@$q_holds),
1780 queue_position => $qpos,
1781 potential_copies => $num_potentials,
1782 status => _hold_status( $e, $hold ),
1783 estimated_wait => int($estimated_wait)
1788 sub fetch_open_hold_by_current_copy {
1791 my $hold = $apputils->simplereq(
1793 'open-ils.cstore.direct.action.hold_request.search.atomic',
1794 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1795 return $hold->[0] if ref($hold);
1799 sub fetch_related_holds {
1802 return $apputils->simplereq(
1804 'open-ils.cstore.direct.action.hold_request.search.atomic',
1805 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1809 __PACKAGE__->register_method(
1810 method => "hold_pull_list",
1811 api_name => "open-ils.circ.hold_pull_list.retrieve",
1813 desc => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1814 'The location is determined by the login session.',
1816 { desc => 'Limit (optional)', type => 'number'},
1817 { desc => 'Offset (optional)', type => 'number'},
1820 desc => 'reference to a list of holds, or event on failure',
1825 __PACKAGE__->register_method(
1826 method => "hold_pull_list",
1827 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
1829 desc => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
1830 'The location is determined by the login session.',
1832 { desc => 'Limit (optional)', type => 'number'},
1833 { desc => 'Offset (optional)', type => 'number'},
1836 desc => 'reference to a list of holds, or event on failure',
1841 __PACKAGE__->register_method(
1842 method => "hold_pull_list",
1843 api_name => "open-ils.circ.hold_pull_list.retrieve.count",
1845 desc => 'Returns a count of holds that need to be "pulled" by a given location. ' .
1846 'The location is determined by the login session.',
1848 { desc => 'Limit (optional)', type => 'number'},
1849 { desc => 'Offset (optional)', type => 'number'},
1852 desc => 'Holds count (integer), or event on failure',
1858 __PACKAGE__->register_method(
1859 method => "hold_pull_list",
1861 # TODO: tag with api_level 2 once fully supported
1862 api_name => "open-ils.circ.hold_pull_list.fleshed.stream",
1864 desc => q/Returns a stream of fleshed holds that need to be
1865 "pulled" by a given location. The location is
1866 determined by the login session.
1867 This API calls always run in authoritative mode./,
1869 { desc => 'Limit (optional)', type => 'number'},
1870 { desc => 'Offset (optional)', type => 'number'},
1873 desc => 'Stream of holds holds, or event on failure',
1878 sub hold_pull_list {
1879 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1880 my( $reqr, $evt ) = $U->checkses($authtoken);
1881 return $evt if $evt;
1883 my $org = $reqr->ws_ou || $reqr->home_ou;
1884 # the perm locaiton shouldn't really matter here since holds
1885 # will exist all over and VIEW_HOLDS should be universal
1886 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1887 return $evt if $evt;
1889 if($self->api_name =~ /count/) {
1891 my $count = $U->storagereq(
1892 'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1893 $org, $limit, $offset );
1895 $logger->info("Grabbing pull list for org unit $org with $count items");
1898 } elsif( $self->api_name =~ /id_list/ ) {
1900 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1901 $org, $limit, $offset );
1903 } elsif ($self->api_name =~ /fleshed/) {
1905 my $ids = $U->storagereq(
1906 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1907 $org, $limit, $offset );
1909 my $e = new_editor(xact => 1, requestor => $reqr);
1910 $conn->respond(uber_hold_impl($e, $_, {flesh_acpl => 1})) for @$ids;
1912 $conn->respond_complete;
1917 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1918 $org, $limit, $offset );
1922 __PACKAGE__->register_method(
1923 method => "print_hold_pull_list",
1924 api_name => "open-ils.circ.hold_pull_list.print",
1926 desc => 'Returns an HTML-formatted holds pull list',
1928 { desc => 'Authtoken', type => 'string'},
1929 { desc => 'Org unit ID. Optional, defaults to workstation org unit', type => 'number'},
1932 desc => 'HTML string',
1938 sub print_hold_pull_list {
1939 my($self, $client, $auth, $org_id) = @_;
1941 my $e = new_editor(authtoken=>$auth);
1942 return $e->event unless $e->checkauth;
1944 $org_id = (defined $org_id) ? $org_id : $e->requestor->ws_ou;
1945 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1947 my $hold_ids = $U->storagereq(
1948 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1951 return undef unless @$hold_ids;
1953 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1955 # Holds will /NOT/ be in order after this ...
1956 my $holds = $e->search_action_hold_request({id => $hold_ids}, {substream => 1});
1957 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1959 # ... so we must resort.
1960 my $hold_map = +{map { $_->id => $_ } @$holds};
1961 my $sorted_holds = [];
1962 push @$sorted_holds, $hold_map->{$_} foreach @$hold_ids;
1964 return $U->fire_object_event(
1965 undef, "ahr.format.pull_list", $sorted_holds,
1966 $org_id, undef, undef, $client
1971 __PACKAGE__->register_method(
1972 method => "print_hold_pull_list_stream",
1974 api_name => "open-ils.circ.hold_pull_list.print.stream",
1976 desc => 'Returns a stream of fleshed holds',
1978 { desc => 'Authtoken', type => 'string'},
1979 { desc => 'Hash of optional param: Org unit ID (defaults to workstation org unit), limit, offset, sort (array of: acplo.position, prefix, call_number, suffix, request_time)',
1984 desc => 'A stream of fleshed holds',
1990 sub print_hold_pull_list_stream {
1991 my($self, $client, $auth, $params) = @_;
1993 my $e = new_editor(authtoken=>$auth);
1994 return $e->die_event unless $e->checkauth;
1996 delete($$params{org_id}) unless (int($$params{org_id}));
1997 delete($$params{limit}) unless (int($$params{limit}));
1998 delete($$params{offset}) unless (int($$params{offset}));
1999 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2000 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2001 $$params{chunk_size} ||= 10;
2002 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2004 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2005 return $e->die_event unless $e->allowed('VIEW_HOLD', $$params{org_id });
2008 if ($$params{sort} && @{ $$params{sort} }) {
2009 for my $s (@{ $$params{sort} }) {
2010 if ($s eq 'acplo.position') {
2012 "class" => "acplo", "field" => "position",
2013 "transform" => "coalesce", "params" => [999]
2015 } elsif ($s eq 'prefix') {
2016 push @$sort, {"class" => "acnp", "field" => "label_sortkey"};
2017 } elsif ($s eq 'call_number') {
2018 push @$sort, {"class" => "acn", "field" => "label_sortkey"};
2019 } elsif ($s eq 'suffix') {
2020 push @$sort, {"class" => "acns", "field" => "label_sortkey"};
2021 } elsif ($s eq 'request_time') {
2022 push @$sort, {"class" => "ahr", "field" => "request_time"};
2026 push @$sort, {"class" => "ahr", "field" => "request_time"};
2029 my $holds_ids = $e->json_query(
2031 "select" => {"ahr" => ["id"]},
2036 "fkey" => "current_copy",
2038 "circ_lib" => $$params{org_id}, "status" => [0,7]
2043 "fkey" => "call_number",
2057 "fkey" => "circ_lib",
2060 "location" => {"=" => {"+acp" => "location"}}
2069 "capture_time" => undef,
2070 "cancel_time" => undef,
2072 {"expire_time" => undef },
2073 {"expire_time" => {">" => "now"}}
2077 (@$sort ? (order_by => $sort) : ()),
2078 ($$params{limit} ? (limit => $$params{limit}) : ()),
2079 ($$params{offset} ? (offset => $$params{offset}) : ())
2080 }, {"substream" => 1}
2081 ) or return $e->die_event;
2083 $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
2086 for my $hid (@$holds_ids) {
2087 push @chunk, $e->retrieve_action_hold_request([
2091 "ahr" => ["usr", "current_copy"],
2093 "acp" => ["location", "call_number", "parts"],
2094 "acn" => ["record","prefix","suffix"]
2099 if (@chunk >= $$params{chunk_size}) {
2100 $client->respond( \@chunk );
2104 $client->respond_complete( \@chunk ) if (@chunk);
2111 __PACKAGE__->register_method(
2112 method => 'fetch_hold_notify',
2113 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
2116 Returns a list of hold notification objects based on hold id.
2117 @param authtoken The loggin session key
2118 @param holdid The id of the hold whose notifications we want to retrieve
2119 @return An array of hold notification objects, event on error.
2123 sub fetch_hold_notify {
2124 my( $self, $conn, $authtoken, $holdid ) = @_;
2125 my( $requestor, $evt ) = $U->checkses($authtoken);
2126 return $evt if $evt;
2127 my ($hold, $patron);
2128 ($hold, $evt) = $U->fetch_hold($holdid);
2129 return $evt if $evt;
2130 ($patron, $evt) = $U->fetch_user($hold->usr);
2131 return $evt if $evt;
2133 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
2134 return $evt if $evt;
2136 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
2137 return $U->cstorereq(
2138 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
2142 __PACKAGE__->register_method(
2143 method => 'create_hold_notify',
2144 api_name => 'open-ils.circ.hold_notification.create',
2146 Creates a new hold notification object
2147 @param authtoken The login session key
2148 @param notification The hold notification object to create
2149 @return ID of the new object on success, Event on error
2153 sub create_hold_notify {
2154 my( $self, $conn, $auth, $note ) = @_;
2155 my $e = new_editor(authtoken=>$auth, xact=>1);
2156 return $e->die_event unless $e->checkauth;
2158 my $hold = $e->retrieve_action_hold_request($note->hold)
2159 or return $e->die_event;
2160 my $patron = $e->retrieve_actor_user($hold->usr)
2161 or return $e->die_event;
2163 return $e->die_event unless
2164 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
2166 $note->notify_staff($e->requestor->id);
2167 $e->create_action_hold_notification($note) or return $e->die_event;
2172 __PACKAGE__->register_method(
2173 method => 'create_hold_note',
2174 api_name => 'open-ils.circ.hold_note.create',
2176 Creates a new hold request note object
2177 @param authtoken The login session key
2178 @param note The hold note object to create
2179 @return ID of the new object on success, Event on error
2183 sub create_hold_note {
2184 my( $self, $conn, $auth, $note ) = @_;
2185 my $e = new_editor(authtoken=>$auth, xact=>1);
2186 return $e->die_event unless $e->checkauth;
2188 my $hold = $e->retrieve_action_hold_request($note->hold)
2189 or return $e->die_event;
2190 my $patron = $e->retrieve_actor_user($hold->usr)
2191 or return $e->die_event;
2193 return $e->die_event unless
2194 $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
2196 $e->create_action_hold_request_note($note) or return $e->die_event;
2201 __PACKAGE__->register_method(
2202 method => 'reset_hold',
2203 api_name => 'open-ils.circ.hold.reset',
2205 Un-captures and un-targets a hold, essentially returning
2206 it to the state it was in directly after it was placed,
2207 then attempts to re-target the hold
2208 @param authtoken The login session key
2209 @param holdid The id of the hold
2215 my( $self, $conn, $auth, $holdid ) = @_;
2217 my ($hold, $evt) = $U->fetch_hold($holdid);
2218 return $evt if $evt;
2219 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
2220 return $evt if $evt;
2221 $evt = _reset_hold($self, $reqr, $hold);
2222 return $evt if $evt;
2227 __PACKAGE__->register_method(
2228 method => 'reset_hold_batch',
2229 api_name => 'open-ils.circ.hold.reset.batch'
2232 sub reset_hold_batch {
2233 my($self, $conn, $auth, $hold_ids) = @_;
2235 my $e = new_editor(authtoken => $auth);
2236 return $e->event unless $e->checkauth;
2238 for my $hold_id ($hold_ids) {
2240 my $hold = $e->retrieve_action_hold_request(
2241 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
2242 or return $e->event;
2244 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
2245 _reset_hold($self, $e->requestor, $hold);
2253 my ($self, $reqr, $hold) = @_;
2255 my $e = new_editor(xact =>1, requestor => $reqr);
2257 $logger->info("reseting hold ".$hold->id);
2259 my $hid = $hold->id;
2261 if( $hold->capture_time and $hold->current_copy ) {
2263 my $copy = $e->retrieve_asset_copy($hold->current_copy)
2264 or return $e->die_event;
2266 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2267 $logger->info("setting copy to status 'reshelving' on hold retarget");
2268 $copy->status(OILS_COPY_STATUS_RESHELVING);
2269 $copy->editor($e->requestor->id);
2270 $copy->edit_date('now');
2271 $e->update_asset_copy($copy) or return $e->die_event;
2273 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
2275 $logger->warn("! reseting hold [$hid] that is in transit");
2276 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
2279 my $trans = $e->retrieve_action_transit_copy($transid);
2281 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
2282 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
2283 $logger->info("Transit abort completed with result $evt");
2284 unless ("$evt" eq 1) {
2293 $hold->clear_capture_time;
2294 $hold->clear_current_copy;
2295 $hold->clear_shelf_time;
2296 $hold->clear_shelf_expire_time;
2297 $hold->clear_current_shelf_lib;
2299 $e->update_action_hold_request($hold) or return $e->die_event;
2302 $U->simplereq('open-ils.hold-targeter',
2303 'open-ils.hold-targeter.target', {hold => $hold->id});
2309 __PACKAGE__->register_method(
2310 method => 'fetch_open_title_holds',
2311 api_name => 'open-ils.circ.open_holds.retrieve',
2313 Returns a list ids of un-fulfilled holds for a given title id
2314 @param authtoken The login session key
2315 @param id the id of the item whose holds we want to retrieve
2316 @param type The hold type - M, T, I, V, C, F, R
2320 sub fetch_open_title_holds {
2321 my( $self, $conn, $auth, $id, $type, $org ) = @_;
2322 my $e = new_editor( authtoken => $auth );
2323 return $e->event unless $e->checkauth;
2326 $org ||= $e->requestor->ws_ou;
2328 # return $e->search_action_hold_request(
2329 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
2331 # XXX make me return IDs in the future ^--
2332 my $holds = $e->search_action_hold_request(
2335 cancel_time => undef,
2337 fulfillment_time => undef
2341 flesh_hold_transits($holds);
2346 sub flesh_hold_transits {
2348 for my $hold ( @$holds ) {
2350 $apputils->simplereq(
2352 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
2353 { hold => $hold->id, cancel_time => undef },
2354 { order_by => { ahtc => 'id desc' }, limit => 1 }
2360 sub flesh_hold_notices {
2361 my( $holds, $e ) = @_;
2362 $e ||= new_editor();
2364 for my $hold (@$holds) {
2365 my $notices = $e->search_action_hold_notification(
2367 { hold => $hold->id },
2368 { order_by => { anh => 'notify_time desc' } },
2373 $hold->notify_count(scalar(@$notices));
2375 my $n = $e->retrieve_action_hold_notification($$notices[0])
2376 or return $e->event;
2377 $hold->notify_time($n->notify_time);
2383 __PACKAGE__->register_method(
2384 method => 'fetch_captured_holds',
2385 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
2389 Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
2390 @param authtoken The login session key
2391 @param org The org id of the location in question
2392 @param match_copy A specific copy to limit to
2396 __PACKAGE__->register_method(
2397 method => 'fetch_captured_holds',
2398 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
2402 Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
2403 @param authtoken The login session key
2404 @param org The org id of the location in question
2405 @param match_copy A specific copy to limit to
2409 __PACKAGE__->register_method(
2410 method => 'fetch_captured_holds',
2411 api_name => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
2415 Returns list ids of shelf-expired un-fulfilled holds for a given title id
2416 @param authtoken The login session key
2417 @param org The org id of the location in question
2418 @param match_copy A specific copy to limit to
2422 __PACKAGE__->register_method(
2423 method => 'fetch_captured_holds',
2425 'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
2429 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2430 for a given shelf lib
2434 __PACKAGE__->register_method(
2435 method => 'fetch_captured_holds',
2437 'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
2441 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2442 for a given shelf lib
2447 sub fetch_captured_holds {
2448 my( $self, $conn, $auth, $org, $match_copy ) = @_;
2450 my $e = new_editor(authtoken => $auth);
2451 return $e->die_event unless $e->checkauth;
2452 return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
2454 $org ||= $e->requestor->ws_ou;
2456 my $current_copy = { '!=' => undef };
2457 $current_copy = { '=' => $match_copy } if $match_copy;
2460 select => { alhr => ['id'] },
2465 fkey => 'current_copy'
2470 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
2472 capture_time => { "!=" => undef },
2473 current_copy => $current_copy,
2474 fulfillment_time => undef,
2475 current_shelf_lib => $org
2479 if($self->api_name =~ /expired/) {
2480 $query->{'where'}->{'+alhr'}->{'-or'} = {
2481 shelf_expire_time => { '<' => 'today'},
2482 cancel_time => { '!=' => undef },
2485 my $hold_ids = $e->json_query( $query );
2487 if ($self->api_name =~ /wrong_shelf/) {
2488 # fetch holds whose current_shelf_lib is $org, but whose pickup
2489 # lib is some other org unit. Ignore already-retrieved holds.
2491 pickup_lib_changed_on_shelf_holds(
2492 $e, $org, [map {$_->{id}} @$hold_ids]);
2493 # match the layout of other items in $hold_ids
2494 push (@$hold_ids, {id => $_}) for @$wrong_shelf;
2498 for my $hold_id (@$hold_ids) {
2499 if($self->api_name =~ /id_list/) {
2500 $conn->respond($hold_id->{id});
2504 $e->retrieve_action_hold_request([
2508 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
2509 order_by => {anh => 'notify_time desc'}
2519 __PACKAGE__->register_method(
2520 method => "print_expired_holds_stream",
2521 api_name => "open-ils.circ.captured_holds.expired.print.stream",
2525 sub print_expired_holds_stream {
2526 my ($self, $client, $auth, $params) = @_;
2528 # No need to check specific permissions: we're going to call another method
2529 # that will do that.
2530 my $e = new_editor("authtoken" => $auth);
2531 return $e->die_event unless $e->checkauth;
2533 delete($$params{org_id}) unless (int($$params{org_id}));
2534 delete($$params{limit}) unless (int($$params{limit}));
2535 delete($$params{offset}) unless (int($$params{offset}));
2536 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2537 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2538 $$params{chunk_size} ||= 10;
2539 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2541 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2543 my @hold_ids = $self->method_lookup(
2544 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
2545 )->run($auth, $params->{"org_id"});
2550 } elsif (defined $U->event_code($hold_ids[0])) {
2552 return $hold_ids[0];
2555 $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
2558 my @hid_chunk = splice @hold_ids, 0, $params->{"chunk_size"};
2560 my $result_chunk = $e->json_query({
2562 "acp" => ["barcode"],
2564 first_given_name second_given_name family_name alias
2573 "field" => "id", "fkey" => "current_copy",
2576 "field" => "id", "fkey" => "call_number",
2579 "field" => "id", "fkey" => "record"
2583 "acpl" => {"field" => "id", "fkey" => "location"}
2586 "au" => {"field" => "id", "fkey" => "usr"}
2589 "where" => {"+ahr" => {"id" => \@hid_chunk}}
2590 }) or return $e->die_event;
2591 $client->respond($result_chunk);
2598 __PACKAGE__->register_method(
2599 method => "check_title_hold_batch",
2600 api_name => "open-ils.circ.title_hold.is_possible.batch",
2603 desc => '@see open-ils.circ.title_hold.is_possible.batch',
2605 { desc => 'Authentication token', type => 'string'},
2606 { desc => 'Array of Hash of named parameters', type => 'array'},
2609 desc => 'Array of response objects',
2615 sub check_title_hold_batch {
2616 my($self, $client, $authtoken, $param_list, $oargs) = @_;
2617 foreach (@$param_list) {
2618 my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
2619 $client->respond($res);
2625 __PACKAGE__->register_method(
2626 method => "check_title_hold",
2627 api_name => "open-ils.circ.title_hold.is_possible",
2629 desc => 'Determines if a hold were to be placed by a given user, ' .
2630 'whether or not said hold would have any potential copies to fulfill it.' .
2631 'The named paramaters of the second argument include: ' .
2632 'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
2633 'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
2635 { desc => 'Authentication token', type => 'string'},
2636 { desc => 'Hash of named parameters', type => 'object'},
2639 desc => 'List of new message IDs (empty if none)',
2645 =head3 check_title_hold (token, hash)
2647 The named fields in the hash are:
2649 patronid - ID of the hold recipient (required)
2650 depth - hold range depth (default 0)
2651 pickup_lib - destination for hold, fallback value for selection_ou
2652 selection_ou - ID of org_unit establishing hard and soft hold boundary settings
2653 issuanceid - ID of the issuance to be held, required for Issuance level hold
2654 partid - ID of the monograph part to be held, required for monograph part level hold
2655 titleid - ID (BRN) of the title to be held, required for Title level hold
2656 volume_id - required for Volume level hold
2657 copy_id - required for Copy level hold
2658 mrid - required for Meta-record level hold
2659 hold_type - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record (default "T")
2661 All key/value pairs are passed on to do_possibility_checks.
2665 # FIXME: better params checking. what other params are required, if any?
2666 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
2667 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
2668 # used in conditionals, where it may be undefined, causing a warning.
2669 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
2671 sub check_title_hold {
2672 my( $self, $client, $authtoken, $params ) = @_;
2673 my $e = new_editor(authtoken=>$authtoken);
2674 return $e->event unless $e->checkauth;
2676 my %params = %$params;
2677 my $depth = $params{depth} || 0;
2678 $params{depth} = $depth; #define $params{depth} if unset, since it gets used later
2679 my $selection_ou = $params{selection_ou} || $params{pickup_lib};
2680 my $oargs = $params{oargs} || {};
2682 if($oargs->{events}) {
2683 @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
2687 my $patron = $e->retrieve_actor_user($params{patronid})
2688 or return $e->event;
2690 if( $e->requestor->id ne $patron->id ) {
2691 return $e->event unless
2692 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
2695 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
2697 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
2698 or return $e->event;
2700 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
2701 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
2704 my $return_depth = $hard_boundary; # default depth to return on success
2705 if(defined $soft_boundary and $depth < $soft_boundary) {
2706 # work up the tree and as soon as we find a potential copy, use that depth
2707 # also, make sure we don't go past the hard boundary if it exists
2709 # our min boundary is the greater of user-specified boundary or hard boundary
2710 my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
2711 $hard_boundary : $depth;
2713 my $depth = $soft_boundary;
2714 while($depth >= $min_depth) {
2715 $logger->info("performing hold possibility check with soft boundary $depth");
2716 @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
2718 $return_depth = $depth;
2723 } elsif(defined $hard_boundary and $depth < $hard_boundary) {
2724 # there is no soft boundary, enforce the hard boundary if it exists
2725 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
2726 @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
2728 # no boundaries defined, fall back to user specifed boundary or no boundary
2729 $logger->info("performing hold possibility check with no boundary");
2730 @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
2733 my $place_unfillable = 0;
2734 $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
2739 "depth" => $return_depth,
2740 "local_avail" => $status[1]
2742 } elsif ($status[2]) {
2743 my $n = scalar @{$status[2]};
2744 return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2746 return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2752 sub do_possibility_checks {
2753 my($e, $patron, $request_lib, $depth, %params) = @_;
2755 my $issuanceid = $params{issuanceid} || "";
2756 my $partid = $params{partid} || "";
2757 my $titleid = $params{titleid} || "";
2758 my $volid = $params{volume_id};
2759 my $copyid = $params{copy_id};
2760 my $mrid = $params{mrid} || "";
2761 my $pickup_lib = $params{pickup_lib};
2762 my $hold_type = $params{hold_type} || 'T';
2763 my $selection_ou = $params{selection_ou} || $pickup_lib;
2764 my $holdable_formats = $params{holdable_formats};
2765 my $oargs = $params{oargs} || {};
2772 if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
2774 return $e->event unless $copy = $e->retrieve_asset_copy($copyid);
2775 return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
2776 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2778 return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
2779 return verify_copy_for_hold(
2780 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
2783 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2785 return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
2786 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2788 return _check_volume_hold_is_possible(
2789 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2792 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
2794 return _check_title_hold_is_possible(
2795 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
2798 } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
2800 return _check_issuance_hold_is_possible(
2801 $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2804 } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
2806 return _check_monopart_hold_is_possible(
2807 $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2810 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2812 # pasing undef as the depth to filtered_records causes the depth
2813 # of the selection_ou to be used, which is not what we want here.
2816 my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
2818 for my $rec (@$recs) {
2819 @status = _check_title_hold_is_possible(
2820 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
2826 # else { Unrecognized hold_type ! } # FIXME: return error? or 0?
2829 sub MR_filter_records {
2836 my $opac_visible = shift;
2838 my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
2839 return $U->storagereq(
2840 'open-ils.storage.metarecord.filtered_records.atomic',
2841 $m, $f, $org_at_depth, $opac_visible
2844 __PACKAGE__->register_method(
2845 method => 'MR_filter_records',
2846 api_name => 'open-ils.circ.holds.metarecord.filtered_records',
2851 sub create_ranged_org_filter {
2852 my($e, $selection_ou, $depth) = @_;
2854 # find the orgs from which this hold may be fulfilled,
2855 # based on the selection_ou and depth
2857 my $top_org = $e->search_actor_org_unit([
2858 {parent_ou => undef},
2859 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
2862 return () if $depth == $top_org->ou_type->depth;
2864 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
2865 %org_filter = (circ_lib => []);
2866 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
2868 $logger->info("hold org filter at depth $depth and selection_ou ".
2869 "$selection_ou created list of @{$org_filter{circ_lib}}");
2875 sub _check_title_hold_is_possible {
2876 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
2877 # $holdable_formats is now unused. We pre-filter the MR's records.
2879 my $e = new_editor();
2880 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2882 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2883 my $copies = $e->json_query(
2885 select => { acp => ['id', 'circ_lib'] },
2890 fkey => 'call_number',
2891 filter => { record => $titleid }
2895 filter => { holdable => 't', deleted => 'f' },
2898 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' },
2899 acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2903 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2904 '+acpm' => { target_copy => undef } # ignore part-linked copies
2909 $logger->info("title possible found ".scalar(@$copies)." potential copies");
2913 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2914 "payload" => {"fail_part" => "no_ultimate_items"}
2919 # -----------------------------------------------------------------------
2920 # sort the copies into buckets based on their circ_lib proximity to
2921 # the patron's home_ou.
2922 # -----------------------------------------------------------------------
2924 my $home_org = $patron->home_ou;
2925 my $req_org = $request_lib->id;
2927 $prox_cache{$home_org} =
2928 $e->search_actor_org_unit_proximity({from_org => $home_org})
2929 unless $prox_cache{$home_org};
2930 my $home_prox = $prox_cache{$home_org};
2931 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2934 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2935 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2937 my @keys = sort { $a <=> $b } keys %buckets;
2940 if( $home_org ne $req_org ) {
2941 # -----------------------------------------------------------------------
2942 # shove the copies close to the request_lib into the primary buckets
2943 # directly before the farthest away copies. That way, they are not
2944 # given priority, but they are checked before the farthest copies.
2945 # -----------------------------------------------------------------------
2946 $prox_cache{$req_org} =
2947 $e->search_actor_org_unit_proximity({from_org => $req_org})
2948 unless $prox_cache{$req_org};
2949 my $req_prox = $prox_cache{$req_org};
2952 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2953 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2955 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2956 my $new_key = $highest_key - 0.5; # right before the farthest prox
2957 my @keys2 = sort { $a <=> $b } keys %buckets2;
2958 for my $key (@keys2) {
2959 last if $key >= $highest_key;
2960 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2964 @keys = sort { $a <=> $b } keys %buckets;
2969 my $age_protect_only = 0;
2970 OUTER: for my $key (@keys) {
2971 my @cps = @{$buckets{$key}};
2973 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2975 for my $copyid (@cps) {
2977 next if $seen{$copyid};
2978 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2979 my $copy = $e->retrieve_asset_copy($copyid);
2980 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2982 unless($title) { # grab the title if we don't already have it
2983 my $vol = $e->retrieve_asset_call_number(
2984 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2985 $title = $vol->record;
2988 @status = verify_copy_for_hold(
2989 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2991 $age_protect_only ||= $status[3];
2992 last OUTER if $status[0];
2996 $status[3] = $age_protect_only;
3000 sub _check_issuance_hold_is_possible {
3001 my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3003 my $e = new_editor();
3004 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
3006 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
3007 my $copies = $e->json_query(
3009 select => { acp => ['id', 'circ_lib'] },
3015 filter => { issuance => $issuanceid }
3019 filter => { holdable => 't', deleted => 'f' },
3022 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3026 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3032 $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
3036 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3037 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3042 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3043 "payload" => {"fail_part" => "no_ultimate_items"}
3051 # -----------------------------------------------------------------------
3052 # sort the copies into buckets based on their circ_lib proximity to
3053 # the patron's home_ou.
3054 # -----------------------------------------------------------------------
3056 my $home_org = $patron->home_ou;
3057 my $req_org = $request_lib->id;
3059 $prox_cache{$home_org} =
3060 $e->search_actor_org_unit_proximity({from_org => $home_org})
3061 unless $prox_cache{$home_org};
3062 my $home_prox = $prox_cache{$home_org};
3063 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3066 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3067 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3069 my @keys = sort { $a <=> $b } keys %buckets;
3072 if( $home_org ne $req_org ) {
3073 # -----------------------------------------------------------------------
3074 # shove the copies close to the request_lib into the primary buckets
3075 # directly before the farthest away copies. That way, they are not
3076 # given priority, but they are checked before the farthest copies.
3077 # -----------------------------------------------------------------------
3078 $prox_cache{$req_org} =
3079 $e->search_actor_org_unit_proximity({from_org => $req_org})
3080 unless $prox_cache{$req_org};
3081 my $req_prox = $prox_cache{$req_org};
3084 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3085 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3087 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
3088 my $new_key = $highest_key - 0.5; # right before the farthest prox
3089 my @keys2 = sort { $a <=> $b } keys %buckets2;
3090 for my $key (@keys2) {
3091 last if $key >= $highest_key;
3092 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3096 @keys = sort { $a <=> $b } keys %buckets;
3101 my $age_protect_only = 0;
3102 OUTER: for my $key (@keys) {
3103 my @cps = @{$buckets{$key}};
3105 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3107 for my $copyid (@cps) {
3109 next if $seen{$copyid};
3110 $seen{$copyid} = 1; # there could be dupes given the merged buckets
3111 my $copy = $e->retrieve_asset_copy($copyid);
3112 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3114 unless($title) { # grab the title if we don't already have it
3115 my $vol = $e->retrieve_asset_call_number(
3116 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3117 $title = $vol->record;
3120 @status = verify_copy_for_hold(
3121 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3123 $age_protect_only ||= $status[3];
3124 last OUTER if $status[0];
3129 if (!defined($empty_ok)) {
3130 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3131 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3134 return (1,0) if ($empty_ok);
3136 $status[3] = $age_protect_only;
3140 sub _check_monopart_hold_is_possible {
3141 my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3143 my $e = new_editor();
3144 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
3146 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
3147 my $copies = $e->json_query(
3149 select => { acp => ['id', 'circ_lib'] },
3153 field => 'target_copy',
3155 filter => { part => $partid }
3159 filter => { holdable => 't', deleted => 'f' },
3162 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3166 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3172 $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
3176 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3177 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3182 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3183 "payload" => {"fail_part" => "no_ultimate_items"}
3191 # -----------------------------------------------------------------------
3192 # sort the copies into buckets based on their circ_lib proximity to
3193 # the patron's home_ou.
3194 # -----------------------------------------------------------------------
3196 my $home_org = $patron->home_ou;
3197 my $req_org = $request_lib->id;
3199 $prox_cache{$home_org} =
3200 $e->search_actor_org_unit_proximity({from_org => $home_org})
3201 unless $prox_cache{$home_org};
3202 my $home_prox = $prox_cache{$home_org};
3203 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3206 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3207 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3209 my @keys = sort { $a <=> $b } keys %buckets;
3212 if( $home_org ne $req_org ) {
3213 # -----------------------------------------------------------------------
3214 # shove the copies close to the request_lib into the primary buckets
3215 # directly before the farthest away copies. That way, they are not
3216 # given priority, but they are checked before the farthest copies.
3217 # -----------------------------------------------------------------------
3218 $prox_cache{$req_org} =
3219 $e->search_actor_org_unit_proximity({from_org => $req_org})
3220 unless $prox_cache{$req_org};
3221 my $req_prox = $prox_cache{$req_org};
3224 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3225 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3227 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
3228 my $new_key = $highest_key - 0.5; # right before the farthest prox
3229 my @keys2 = sort { $a <=> $b } keys %buckets2;
3230 for my $key (@keys2) {
3231 last if $key >= $highest_key;
3232 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3236 @keys = sort { $a <=> $b } keys %buckets;
3241 my $age_protect_only = 0;
3242 OUTER: for my $key (@keys) {
3243 my @cps = @{$buckets{$key}};
3245 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3247 for my $copyid (@cps) {
3249 next if $seen{$copyid};
3250 $seen{$copyid} = 1; # there could be dupes given the merged buckets
3251 my $copy = $e->retrieve_asset_copy($copyid);
3252 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3254 unless($title) { # grab the title if we don't already have it
3255 my $vol = $e->retrieve_asset_call_number(
3256 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3257 $title = $vol->record;
3260 @status = verify_copy_for_hold(
3261 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3263 $age_protect_only ||= $status[3];
3264 last OUTER if $status[0];
3269 if (!defined($empty_ok)) {
3270 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3271 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3274 return (1,0) if ($empty_ok);
3276 $status[3] = $age_protect_only;
3281 sub _check_volume_hold_is_possible {
3282 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3283 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
3284 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
3285 $logger->info("checking possibility of volume hold for volume ".$vol->id);
3287 my $filter_copies = [];
3288 for my $copy (@$copies) {
3289 # ignore part-mapped copies for regular volume level holds
3290 push(@$filter_copies, $copy) unless
3291 new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
3293 $copies = $filter_copies;
3298 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3299 "payload" => {"fail_part" => "no_ultimate_items"}
3305 my $age_protect_only = 0;
3306 for my $copy ( @$copies ) {
3307 @status = verify_copy_for_hold(
3308 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
3309 $age_protect_only ||= $status[3];
3312 $status[3] = $age_protect_only;
3318 sub verify_copy_for_hold {
3319 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
3320 # $oargs should be undef unless we're overriding.
3321 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
3322 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3325 requestor => $requestor,
3328 title_descriptor => $title->fixed_fields,
3329 pickup_lib => $pickup_lib,
3330 request_lib => $request_lib,
3332 show_event_list => 1
3336 # Check for override permissions on events.
3337 if ($oargs && $permitted && scalar @$permitted) {
3338 # Remove the events from permitted that we can override.
3339 if ($oargs->{events}) {
3340 foreach my $evt (@{$oargs->{events}}) {
3341 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3344 # Now, we handle the override all case by checking remaining
3345 # events against override permissions.
3346 if (scalar @$permitted && $oargs->{all}) {
3347 # Pre-set events and failed members of oargs to empty
3348 # arrays, if they are not set, yet.
3349 $oargs->{events} = [] unless ($oargs->{events});
3350 $oargs->{failed} = [] unless ($oargs->{failed});
3351 # When we're done with these checks, we swap permitted
3352 # with a reference to @disallowed.
3353 my @disallowed = ();
3354 foreach my $evt (@{$permitted}) {
3355 # Check if we've already seen the event in this
3356 # session and it failed.
3357 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3358 push(@disallowed, $evt);
3360 # We have to check if the requestor has the
3361 # override permission.
3363 # AppUtils::check_user_perms returns the perm if
3364 # the user doesn't have it, undef if they do.
3365 if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3366 push(@disallowed, $evt);
3367 push(@{$oargs->{failed}}, $evt->{textcode});
3369 push(@{$oargs->{events}}, $evt->{textcode});
3373 $permitted = \@disallowed;
3377 my $age_protect_only = 0;
3378 if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3379 $age_protect_only = 1;
3383 (not scalar @$permitted), # true if permitted is an empty arrayref
3384 ( # XXX This test is of very dubious value; someone should figure
3385 # out what if anything is checking this value
3386 ($copy->circ_lib == $pickup_lib) and
3387 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3396 sub find_nearest_permitted_hold {
3399 my $editor = shift; # CStoreEditor object
3400 my $copy = shift; # copy to target
3401 my $user = shift; # staff
3402 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3404 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3406 my $bc = $copy->barcode;
3408 # find any existing holds that already target this copy
3409 my $old_holds = $editor->search_action_hold_request(
3410 { current_copy => $copy->id,
3411 cancel_time => undef,
3412 capture_time => undef
3416 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3418 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3419 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3421 my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3423 # the nearest_hold API call now needs this
3424 $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3425 unless ref $copy->call_number;
3427 # search for what should be the best holds for this copy to fulfill
3428 my $best_holds = $U->storagereq(
3429 "open-ils.storage.action.hold_request.nearest_hold.atomic",
3430 $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3432 # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3434 for my $holdid (@$old_holds) {
3435 next unless $holdid;
3436 push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3440 unless(@$best_holds) {
3441 $logger->info("circulator: no suitable holds found for copy $bc");
3442 return (undef, $evt);
3448 # for each potential hold, we have to run the permit script
3449 # to make sure the hold is actually permitted.
3452 for my $holdid (@$best_holds) {
3453 next unless $holdid;
3454 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3456 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3457 # Force and recall holds bypass all rules
3458 if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3462 my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3463 my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3465 $reqr_cache{$hold->requestor} = $reqr;
3466 $org_cache{$hold->request_lib} = $rlib;
3468 # see if this hold is permitted
3469 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3471 patron_id => $hold->usr,
3474 pickup_lib => $hold->pickup_lib,
3475 request_lib => $rlib,
3487 unless( $best_hold ) { # no "good" permitted holds were found
3489 $logger->info("circulator: no suitable holds found for copy $bc");
3490 return (undef, $evt);
3493 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3495 # indicate a permitted hold was found
3496 return $best_hold if $check_only;
3498 # we've found a permitted hold. we need to "grab" the copy
3499 # to prevent re-targeted holds (next part) from re-grabbing the copy
3500 $best_hold->current_copy($copy->id);
3501 $editor->update_action_hold_request($best_hold)
3502 or return (undef, $editor->event);
3507 # re-target any other holds that already target this copy
3508 for my $old_hold (@$old_holds) {
3509 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3510 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3511 $old_hold->id." after a better hold [".$best_hold->id."] was found");
3512 $old_hold->clear_current_copy;
3513 $old_hold->clear_prev_check_time;
3514 $editor->update_action_hold_request($old_hold)
3515 or return (undef, $editor->event);
3516 push(@retarget, $old_hold->id);
3519 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3527 __PACKAGE__->register_method(
3528 method => 'all_rec_holds',
3529 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3533 my( $self, $conn, $auth, $title_id, $args ) = @_;
3535 my $e = new_editor(authtoken=>$auth);
3536 $e->checkauth or return $e->event;
3537 $e->allowed('VIEW_HOLD') or return $e->event;
3540 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
3541 $args->{cancel_time} = undef;
3544 metarecord_holds => []
3546 , volume_holds => []
3548 , recall_holds => []
3551 , issuance_holds => []
3554 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3556 $resp->{metarecord_holds} = $e->search_action_hold_request(
3557 { hold_type => OILS_HOLD_TYPE_METARECORD,
3558 target => $mr_map->metarecord,
3564 $resp->{title_holds} = $e->search_action_hold_request(
3566 hold_type => OILS_HOLD_TYPE_TITLE,
3567 target => $title_id,
3571 my $parts = $e->search_biblio_monograph_part(
3577 $resp->{part_holds} = $e->search_action_hold_request(
3579 hold_type => OILS_HOLD_TYPE_MONOPART,
3585 my $subs = $e->search_serial_subscription(
3586 { record_entry => $title_id }, {idlist=>1});
3589 my $issuances = $e->search_serial_issuance(
3590 {subscription => $subs}, {idlist=>1}
3594 $resp->{issuance_holds} = $e->search_action_hold_request(
3596 hold_type => OILS_HOLD_TYPE_ISSUANCE,
3597 target => $issuances,
3604 my $vols = $e->search_asset_call_number(
3605 { record => $title_id, deleted => 'f' }, {idlist=>1});
3607 return $resp unless @$vols;
3609 $resp->{volume_holds} = $e->search_action_hold_request(
3611 hold_type => OILS_HOLD_TYPE_VOLUME,
3616 my $copies = $e->search_asset_copy(
3617 { call_number => $vols, deleted => 'f' }, {idlist=>1});
3619 return $resp unless @$copies;
3621 $resp->{copy_holds} = $e->search_action_hold_request(
3623 hold_type => OILS_HOLD_TYPE_COPY,
3628 $resp->{recall_holds} = $e->search_action_hold_request(
3630 hold_type => OILS_HOLD_TYPE_RECALL,
3635 $resp->{force_holds} = $e->search_action_hold_request(
3637 hold_type => OILS_HOLD_TYPE_FORCE,
3645 __PACKAGE__->register_method(
3646 method => 'stream_wide_holds',
3649 api_name => 'open-ils.circ.hold.wide_hash.stream'
3652 sub stream_wide_holds {
3653 my($self, $client, $auth, $restrictions, $order_by, $limit, $offset, $options) = @_;
3656 my $e = new_editor(authtoken=>$auth);
3657 $e->checkauth or return $e->event;
3658 $e->allowed('VIEW_HOLD') or return $e->event;
3660 if ($options->{recently_canceled}) {
3661 # Map the the recently canceled holds filter into values
3662 # wide-stream understands.
3663 my $filter = recently_canceled_holds_filter($e);
3664 $restrictions->{$_} =
3665 $filter->{where}->{$_} for keys %{$filter->{where}};
3667 $limit = $filter->{limit} if $filter->{limit};
3670 my $filters = OpenSRF::Utils::JSON->perl2JSON($restrictions);
3671 $logger->info("WIDE HOLD FILTERS: $filters");
3673 my $st = OpenSRF::AppSession->create('open-ils.storage');
3674 my $req = $st->request(
3675 'open-ils.storage.action.live_holds.wide_hash.atomic',
3676 $restrictions, $order_by, $limit, $offset
3679 my $results = $req->recv;
3684 if(UNIVERSAL::isa($results,"Error")) {
3685 throw OpenSRF::EX::ERROR ("Error fetch hold shelf list");
3688 my @rows = @{ $results->content };
3690 # Force immediate send of count response
3691 my $mbc = $client->max_bundle_count;
3692 $client->max_bundle_count(1);
3693 $client->respond(shift @rows);
3694 $client->max_bundle_count($mbc);
3696 foreach my $hold (@rows) {
3697 $client->respond($hold) if $hold;
3700 $client->respond_complete;
3706 __PACKAGE__->register_method(
3707 method => 'uber_hold',
3709 api_name => 'open-ils.circ.hold.details.retrieve'
3713 my($self, $client, $auth, $hold_id, $args) = @_;
3714 my $e = new_editor(authtoken=>$auth);
3715 $e->checkauth or return $e->event;
3716 return uber_hold_impl($e, $hold_id, $args);
3719 __PACKAGE__->register_method(
3720 method => 'batch_uber_hold',
3723 api_name => 'open-ils.circ.hold.details.batch.retrieve'
3726 sub batch_uber_hold {
3727 my($self, $client, $auth, $hold_ids, $args) = @_;
3728 my $e = new_editor(authtoken=>$auth);
3729 $e->checkauth or return $e->event;
3730 $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3734 sub uber_hold_impl {
3735 my($e, $hold_id, $args) = @_;
3738 my $flesh_fields = ['current_copy', 'usr', 'notes'];
3739 push (@$flesh_fields, 'requestor') if $args->{include_requestor};
3740 push (@$flesh_fields, 'cancel_cause') if $args->{include_cancel_cause};
3741 push (@$flesh_fields, 'sms_carrier') if $args->{include_sms_carrier};
3743 my $hold = $e->retrieve_action_hold_request([
3745 {flesh => 1, flesh_fields => {ahr => $flesh_fields}}
3746 ]) or return $e->event;
3748 if($hold->usr->id ne $e->requestor->id) {
3749 # caller is asking for someone else's hold
3750 $e->allowed('VIEW_HOLD') or return $e->event;
3751 $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3752 [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3755 # caller is asking for own hold, but may not have permission to view staff notes
3756 unless($e->allowed('VIEW_HOLD')) {
3757 $hold->notes( # filter out any staff notes (unless marked as public)
3758 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3762 my $user = $hold->usr;
3763 $hold->usr($user->id);
3766 my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args);
3768 flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3769 flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3771 my $details = retrieve_hold_queue_status_impl($e, $hold);
3772 $hold->usr($user) if $args->{include_usr}; # re-flesh
3777 ($copy ? (copy => $copy) : ()),
3778 ($volume ? (volume => $volume) : ()),
3779 ($issuance ? (issuance => $issuance) : ()),
3780 ($part ? (part => $part) : ()),
3781 ($args->{include_bre} ? (bre => $bre) : ()),
3782 ($args->{suppress_mvr} ? () : (mvr => $mvr)),
3786 $resp->{copy}->location(
3787 $e->retrieve_asset_copy_location($resp->{copy}->location))
3788 if $resp->{copy} and $args->{flesh_acpl};
3790 unless($args->{suppress_patron_details}) {
3791 my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3792 $resp->{patron_first} = $user->first_given_name,
3793 $resp->{patron_last} = $user->family_name,
3794 $resp->{patron_barcode} = $card->barcode,
3795 $resp->{patron_alias} = $user->alias,
3803 # -----------------------------------------------------
3804 # Returns the MVR object that represents what the
3806 # -----------------------------------------------------
3808 my( $e, $hold, $args ) = @_;
3816 my $no_mvr = $args->{suppress_mvr};
3818 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3819 $metarecord = $e->retrieve_metabib_metarecord($hold->target)
3820 or return $e->event;
3821 $tid = $metarecord->master_record;
3823 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3824 $tid = $hold->target;
3826 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3827 $volume = $e->retrieve_asset_call_number($hold->target)
3828 or return $e->event;
3829 $tid = $volume->record;
3831 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3832 $issuance = $e->retrieve_serial_issuance([
3834 {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3835 ]) or return $e->event;
3837 $tid = $issuance->subscription->record_entry;
3839 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3840 $part = $e->retrieve_biblio_monograph_part([
3842 ]) or return $e->event;
3844 $tid = $part->record;
3846 } 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 ) {
3847 $copy = $e->retrieve_asset_copy([
3849 {flesh => 1, flesh_fields => {acp => ['call_number']}}
3850 ]) or return $e->event;
3852 $volume = $copy->call_number;
3853 $tid = $volume->record;
3856 if(!$copy and ref $hold->current_copy ) {
3857 $copy = $hold->current_copy;
3858 $hold->current_copy($copy->id) unless $args->{include_current_copy};
3861 if(!$volume and $copy) {
3862 $volume = $e->retrieve_asset_call_number($copy->call_number);
3865 # TODO return metarcord mvr for M holds
3866 my $title = $e->retrieve_biblio_record_entry($tid);
3867 return ( ($no_mvr) ? undef : $U->record_to_mvr($title),
3868 $volume, $copy, $issuance, $part, $title, $metarecord);
3871 __PACKAGE__->register_method(
3872 method => 'clear_shelf_cache',
3873 api_name => 'open-ils.circ.hold.clear_shelf.get_cache',
3877 Returns the holds processed with the given cache key
3882 sub clear_shelf_cache {
3883 my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3884 my $e = new_editor(authtoken => $auth, xact => 1);
3885 return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3888 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3890 my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3893 $logger->info("no hold data found in cache"); # XXX TODO return event
3899 foreach (keys %$hold_data) {
3900 $maximum += scalar(@{ $hold_data->{$_} });
3902 $client->respond({"maximum" => $maximum, "progress" => 0});
3904 for my $action (sort keys %$hold_data) {
3905 while (@{$hold_data->{$action}}) {
3906 my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3908 my $result_chunk = $e->json_query({
3910 "acp" => ["barcode"],
3912 first_given_name second_given_name family_name alias
3915 "acnp" => [{column => "label", alias => "prefix"}],
3916 "acns" => [{column => "label", alias => "suffix"}],
3924 "field" => "id", "fkey" => "current_copy",
3927 "field" => "id", "fkey" => "call_number",
3930 "field" => "id", "fkey" => "record"
3933 "field" => "id", "fkey" => "prefix"
3936 "field" => "id", "fkey" => "suffix"
3940 "acpl" => {"field" => "id", "fkey" => "location"}
3943 "au" => {"field" => "id", "fkey" => "usr"}
3946 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3947 }, {"substream" => 1}) or return $e->die_event;
3951 +{"action" => $action, "hold_details" => $_}
3962 __PACKAGE__->register_method(
3963 method => 'clear_shelf_process',
3965 api_name => 'open-ils.circ.hold.clear_shelf.process',
3968 1. Find all holds that have expired on the holds shelf
3970 3. If a clear-shelf status is configured, put targeted copies into this status
3971 4. Divide copies into 3 groups: items to transit, items to reshelve, and items
3972 that are needed for holds. No subsequent action is taken on the holds
3973 or items after grouping.
3978 sub clear_shelf_process {
3979 my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
3981 my $e = new_editor(authtoken=>$auth);
3982 $e->checkauth or return $e->die_event;
3983 my $cache = OpenSRF::Utils::Cache->new('global');
3985 $org_id ||= $e->requestor->ws_ou;
3986 $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
3988 my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
3990 my @hold_ids = $self->method_lookup(
3991 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
3992 )->run($auth, $org_id, $match_copy);
3997 my @canceled_holds; # newly canceled holds
3998 $chunk_size ||= 25; # chunked status updates
3999 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
4002 for my $hold_id (@hold_ids) {
4004 $logger->info("Clear shelf processing hold $hold_id");
4006 my $hold = $e->retrieve_action_hold_request([
4009 flesh_fields => {ahr => ['current_copy']}
4013 if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
4014 $hold->cancel_time('now');
4015 $hold->cancel_cause(2); # Hold Shelf expiration
4016 $e->update_action_hold_request($hold) or return $e->die_event;
4017 push(@canceled_holds, $hold_id);
4020 my $copy = $hold->current_copy;
4022 if($copy_status or $copy_status == 0) {
4023 # if a clear-shelf copy status is defined, update the copy
4024 $copy->status($copy_status);
4025 $copy->edit_date('now');
4026 $copy->editor($e->requestor->id);
4027 $e->update_asset_copy($copy) or return $e->die_event;
4030 push(@holds, $hold);
4031 $client->respond({maximum => int(scalar(@holds)), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
4040 pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
4043 for my $hold (@holds) {
4045 my $copy = $hold->current_copy;
4046 my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
4048 if($alt_hold and !$match_copy) {
4050 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
4052 } elsif($copy->circ_lib != $e->requestor->ws_ou) {
4054 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
4058 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
4062 my $cache_key = md5_hex(time . $$ . rand());
4063 $logger->info("clear_shelf_cache: storing under $cache_key");
4064 $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours. configurable?
4066 # tell the client we're done
4067 $client->respond_complete({cache_key => $cache_key});
4070 # fire off the hold cancelation trigger and wait for response so don't flood the service
4072 # refetch the holds to pick up the caclulated cancel_time,
4073 # which may be needed by Action/Trigger
4075 my $updated_holds = [];
4076 $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
4079 $U->create_events_for_hook(
4080 'hold_request.cancel.expire_holds_shelf',
4081 $_, $org_id, undef, undef, 1) for @$updated_holds;
4084 # tell the client we're done
4085 $client->respond_complete;
4089 # returns IDs for holds that are on the holds shelf but
4090 # have had their pickup_libs change while on the shelf.
4091 sub pickup_lib_changed_on_shelf_holds {
4094 my $ignore_holds = shift;
4095 $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
4098 select => { alhr => ['id'] },
4103 fkey => 'current_copy'
4108 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
4110 capture_time => { "!=" => undef },
4111 fulfillment_time => undef,
4112 current_shelf_lib => $org_id,
4113 pickup_lib => {'!=' => {'+alhr' => 'current_shelf_lib'}}
4118 $query->{where}->{'+alhr'}->{id} =
4119 {'not in' => $ignore_holds} if @$ignore_holds;
4121 my $hold_ids = $e->json_query($query);
4122 return [ map { $_->{id} } @$hold_ids ];
4125 __PACKAGE__->register_method(
4126 method => 'usr_hold_summary',
4127 api_name => 'open-ils.circ.holds.user_summary',
4129 Returns a summary of holds statuses for a given user
4133 sub usr_hold_summary {
4134 my($self, $conn, $auth, $user_id) = @_;
4136 my $e = new_editor(authtoken=>$auth);
4137 $e->checkauth or return $e->event;
4138 $e->allowed('VIEW_HOLD') or return $e->event;
4140 my $holds = $e->search_action_hold_request(
4143 fulfillment_time => undef,
4144 cancel_time => undef,
4148 my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
4149 $summary{_hold_status($e, $_)} += 1 for @$holds;
4155 __PACKAGE__->register_method(
4156 method => 'hold_has_copy_at',
4157 api_name => 'open-ils.circ.hold.has_copy_at',
4160 'Returns the ID of the found copy and name of the shelving location if there is ' .
4161 'an available copy at the specified org unit. Returns empty hash otherwise. ' .
4162 'The anticipated use for this method is to determine whether an item is ' .
4163 'available at the library where the user is placing the hold (or, alternatively, '.
4164 'at the pickup library) to encourage bypassing the hold placement and just ' .
4165 'checking out the item.' ,
4167 { desc => 'Authentication Token', type => 'string' },
4168 { desc => 'Method Arguments. Options include: hold_type, hold_target, org_unit. '
4169 . 'hold_type is the hold type code (T, V, C, M, ...). '
4170 . 'hold_target is the identifier of the hold target object. '
4171 . 'org_unit is org unit ID.',
4176 desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
4182 sub hold_has_copy_at {
4183 my($self, $conn, $auth, $args) = @_;
4185 my $e = new_editor(authtoken=>$auth);
4186 $e->checkauth or return $e->event;
4188 my $hold_type = $$args{hold_type};
4189 my $hold_target = $$args{hold_target};
4190 my $org_unit = $$args{org_unit};
4193 select => {acp => ['id'], acpl => ['name']},
4198 filter => { holdable => 't', deleted => 'f' },
4201 ccs => {field => 'id', filter => { holdable => 't'}, fkey => 'status' }
4204 where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit, status => [0,7]}},
4208 if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
4210 $query->{where}->{'+acp'}->{id} = $hold_target;
4212 } elsif($hold_type eq 'V') {
4214 $query->{where}->{'+acp'}->{call_number} = $hold_target;
4216 } elsif($hold_type eq 'P') {
4218 $query->{from}->{acp}->{acpm} = {
4219 field => 'target_copy',
4221 filter => {part => $hold_target},
4224 } elsif($hold_type eq 'I') {
4226 $query->{from}->{acp}->{sitem} = {
4229 filter => {issuance => $hold_target},
4232 } elsif($hold_type eq 'T') {
4234 $query->{from}->{acp}->{acn} = {
4236 fkey => 'call_number',
4240 filter => {id => $hold_target},
4248 $query->{from}->{acp}->{acn} = {
4250 fkey => 'call_number',
4259 filter => {metarecord => $hold_target},
4267 my $res = $e->json_query($query)->[0] or return {};
4268 return {copy => $res->{id}, location => $res->{name}} if $res;
4272 # returns true if the user already has an item checked out
4273 # that could be used to fulfill the requested hold.
4274 sub hold_item_is_checked_out {
4275 my($e, $user_id, $hold_type, $hold_target) = @_;
4278 select => {acp => ['id']},
4279 from => {acp => {}},
4283 in => { # copies for circs the user has checked out
4284 select => {circ => ['target_copy']},
4288 checkin_time => undef,
4290 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
4291 {stop_fines => undef}
4301 if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
4303 $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
4305 } elsif($hold_type eq 'V') {
4307 $query->{where}->{'+acp'}->{call_number} = $hold_target;
4309 } elsif($hold_type eq 'P') {
4311 $query->{from}->{acp}->{acpm} = {
4312 field => 'target_copy',
4314 filter => {part => $hold_target},
4317 } elsif($hold_type eq 'I') {
4319 $query->{from}->{acp}->{sitem} = {
4322 filter => {issuance => $hold_target},
4325 } elsif($hold_type eq 'T') {
4327 $query->{from}->{acp}->{acn} = {
4329 fkey => 'call_number',
4333 filter => {id => $hold_target},
4341 $query->{from}->{acp}->{acn} = {
4343 fkey => 'call_number',
4352 filter => {metarecord => $hold_target},
4360 return $e->json_query($query)->[0];
4363 __PACKAGE__->register_method(
4364 method => 'change_hold_title',
4365 api_name => 'open-ils.circ.hold.change_title',
4368 Updates all title level holds targeting the specified bibs to point a new bib./,
4370 { desc => 'Authentication Token', type => 'string' },
4371 { desc => 'New Target Bib Id', type => 'number' },
4372 { desc => 'Old Target Bib Ids', type => 'array' },
4374 return => { desc => '1 on success' }
4378 __PACKAGE__->register_method(
4379 method => 'change_hold_title_for_specific_holds',
4380 api_name => 'open-ils.circ.hold.change_title.specific_holds',
4383 Updates specified holds to target new bib./,
4385 { desc => 'Authentication Token', type => 'string' },
4386 { desc => 'New Target Bib Id', type => 'number' },
4387 { desc => 'Holds Ids for holds to update', type => 'array' },
4389 return => { desc => '1 on success' }
4394 sub change_hold_title {
4395 my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4397 my $e = new_editor(authtoken=>$auth, xact=>1);
4398 return $e->die_event unless $e->checkauth;
4400 my $holds = $e->search_action_hold_request(
4403 capture_time => undef,
4404 cancel_time => undef,
4405 fulfillment_time => undef,
4411 flesh_fields => { ahr => ['usr'] }
4417 for my $hold (@$holds) {
4418 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4419 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4420 $hold->target( $new_bib_id );
4421 $e->update_action_hold_request($hold) or return $e->die_event;
4426 _reset_hold($self, $e->requestor, $_) for @$holds;
4431 sub change_hold_title_for_specific_holds {
4432 my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4434 my $e = new_editor(authtoken=>$auth, xact=>1);
4435 return $e->die_event unless $e->checkauth;
4437 my $holds = $e->search_action_hold_request(
4440 capture_time => undef,
4441 cancel_time => undef,
4442 fulfillment_time => undef,
4448 flesh_fields => { ahr => ['usr'] }
4454 for my $hold (@$holds) {
4455 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4456 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4457 $hold->target( $new_bib_id );
4458 $e->update_action_hold_request($hold) or return $e->die_event;
4463 _reset_hold($self, $e->requestor, $_) for @$holds;
4468 __PACKAGE__->register_method(
4469 method => 'rec_hold_count',
4470 api_name => 'open-ils.circ.bre.holds.count',
4472 desc => q/Returns the total number of holds that target the
4473 selected bib record or its associated copies and call_numbers/,
4475 { desc => 'Bib ID', type => 'number' },
4476 { desc => q/Optional arguments. Supported arguments include:
4477 "pickup_lib_descendant" -> limit holds to those whose pickup
4478 library is equal to or is a child of the provided org unit/,
4482 return => {desc => 'Hold count', type => 'number'}
4486 __PACKAGE__->register_method(
4487 method => 'rec_hold_count',
4488 api_name => 'open-ils.circ.mmr.holds.count',
4490 desc => q/Returns the total number of holds that target the
4491 selected metarecord or its associated copies, call_numbers, and bib records/,
4493 { desc => 'Metarecord ID', type => 'number' },
4495 return => {desc => 'Hold count', type => 'number'}
4499 # XXX Need to add type I holds to these counts
4500 sub rec_hold_count {
4501 my($self, $conn, $target_id, $args) = @_;
4508 filter => {metarecord => $target_id}
4515 filter => { id => $target_id },
4520 if($self->api_name =~ /mmr/) {
4521 delete $bre_join->{bre}->{filter};
4522 $bre_join->{bre}->{join} = $mmr_join;
4528 fkey => 'call_number',
4534 select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4538 cancel_time => undef,
4539 fulfillment_time => undef,
4543 hold_type => [qw/C F R/],
4546 select => {acp => ['id']},
4547 from => { acp => $cn_join }
4557 select => {acn => ['id']},
4558 from => {acn => $bre_join}
4568 select => {bmp => ['id']},
4569 from => {bmp => $bre_join}
4577 target => $target_id
4585 if($self->api_name =~ /mmr/) {
4586 $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4591 select => {bre => ['id']},
4592 from => {bre => $mmr_join}
4598 $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4601 target => $target_id
4607 if (my $pld = $args->{pickup_lib_descendant}) {
4609 my $top_ou = new_editor()->search_actor_org_unit(
4610 {parent_ou => undef}
4611 )->[0]; # XXX Assumes single root node. Not alone in this...
4613 $query->{where}->{'+ahr'}->{pickup_lib} = {
4615 select => {aou => [{
4617 transform => 'actor.org_unit_descendants',
4618 result_field => 'id'
4621 where => {id => $pld}
4623 } if ($pld != $top_ou->id);
4626 # To avoid Internal Server Errors, we get an editor, then run the
4627 # query and check the result. If anything fails, we'll return 0.
4629 if (my $e = new_editor()) {
4630 my $query_result = $e->json_query($query);
4631 if ($query_result && @{$query_result}) {
4632 $result = $query_result->[0]->{count}
4639 # A helper function to calculate a hold's expiration time at a given
4640 # org_unit. Takes the org_unit as an argument and returns either the
4641 # hold expire time as an ISO8601 string or undef if there is no hold
4642 # expiration interval set for the subject ou.
4643 sub calculate_expire_time
4646 my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4648 my $date = DateTime->now->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($interval));
4649 return $U->epoch2ISO8601($date->epoch);
4655 __PACKAGE__->register_method(
4656 method => 'mr_hold_filter_attrs',
4657 api_name => 'open-ils.circ.mmr.holds.filters',
4662 Returns the set of available formats and languages for the
4663 constituent records of the provided metarcord.
4664 If an array of hold IDs is also provided, information about
4665 each is returned as well. This information includes:
4666 1. a slightly easier to read version of holdable_formats
4667 2. attributes describing the set of format icons included
4668 in the set of desired, constituent records.
4671 {desc => 'Metarecord ID', type => 'number'},
4672 {desc => 'Context Org ID', type => 'number'},
4673 {desc => 'Hold ID List', type => 'array'},
4677 Stream of objects. The first will have a 'metarecord' key
4678 containing non-hold-specific metarecord information, subsequent
4679 responses will contain a 'hold' key containing hold-specific
4687 sub mr_hold_filter_attrs {
4688 my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4689 my $e = new_editor();
4691 # by default, return MR / hold attributes for all constituent
4692 # records with holdable copies. If there is a hard boundary,
4693 # though, limit to records with copies within the boundary,
4694 # since anything outside the boundary can never be held.
4697 $org_depth = $U->ou_ancestor_setting_value(
4698 $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4701 # get all org-scoped records w/ holdable copies for this metarecord
4702 my ($bre_ids) = $self->method_lookup(
4703 'open-ils.circ.holds.metarecord.filtered_records')->run(
4704 $mr_id, undef, $org_id, $org_depth);
4706 my $item_lang_attr = 'item_lang'; # configurable?
4707 my $format_attr = $e->retrieve_config_global_flag(
4708 'opac.metarecord.holds.format_attr')->value;
4710 # helper sub for fetching ccvms for a batch of record IDs
4711 sub get_batch_ccvms {
4712 my ($e, $attr, $bre_ids) = @_;
4713 return [] unless $bre_ids and @$bre_ids;
4714 my $vals = $e->search_metabib_record_attr_flat({
4718 return [] unless @$vals;
4719 return $e->search_config_coded_value_map({
4721 code => [map {$_->value} @$vals]
4725 my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4726 my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4731 formats => $formats,
4736 return unless $hold_ids;
4737 my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4738 $icon_attr = $icon_attr ? $icon_attr->value : '';
4740 for my $hold_id (@$hold_ids) {
4741 my $hold = $e->retrieve_action_hold_request($hold_id)
4742 or return $e->event;
4744 next unless $hold->hold_type eq 'M';
4754 # collect the ccvm's for the selected formats / language
4755 # (i.e. the holdable formats) on the MR.
4756 # this assumes a two-key structure for format / language,
4757 # though no assumption is made about the keys themselves.
4758 my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4760 my $format_vals = [];
4761 for my $val (values %$hformats) {
4762 # val is either a single ccvm or an array of them
4763 $val = [$val] unless ref $val eq 'ARRAY';
4764 for my $node (@$val) {
4765 push (@$lang_vals, $node->{_val})
4766 if $node->{_attr} eq $item_lang_attr;
4767 push (@$format_vals, $node->{_val})
4768 if $node->{_attr} eq $format_attr;
4772 # fetch the ccvm's for consistency with the {metarecord} blob
4773 $resp->{hold}{formats} = $e->search_config_coded_value_map({
4774 ctype => $format_attr, code => $format_vals});
4775 $resp->{hold}{langs} = $e->search_config_coded_value_map({
4776 ctype => $item_lang_attr, code => $lang_vals});
4778 # find all of the bib records within this metarcord whose
4779 # format / language match the holdable formats on the hold
4780 my ($bre_ids) = $self->method_lookup(
4781 'open-ils.circ.holds.metarecord.filtered_records')->run(
4782 $hold->target, $hold->holdable_formats,
4783 $hold->selection_ou, $hold->selection_depth);
4785 # now find all of the 'icon' attributes for the records
4786 $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4787 $client->respond($resp);
4793 __PACKAGE__->register_method(
4794 method => "copy_has_holds_count",
4795 api_name => "open-ils.circ.copy.has_holds_count",
4799 Returns the number of holds a paticular copy has
4802 { desc => 'Authentication Token', type => 'string'},
4803 { desc => 'Copy ID', type => 'number'}
4814 sub copy_has_holds_count {
4815 my( $self, $conn, $auth, $copyid ) = @_;
4816 my $e = new_editor(authtoken=>$auth);
4817 return $e->event unless $e->checkauth;
4819 if( $copyid && $copyid > 0 ) {
4820 my $meth = 'retrieve_action_has_holds_count';
4821 my $data = $e->$meth($copyid);
4823 return $data->count();
4829 __PACKAGE__->register_method(
4830 method => "retrieve_holds_by_usr_notify_value_staff",
4831 api_name => "open-ils.circ.holds.retrieve_by_notify_staff",
4833 desc => "Retrieve the hold, for the specified user using the notify value. $ses_is_req_note",
4835 { desc => 'Authentication token', type => 'string' },
4836 { desc => 'User ID', type => 'number' },
4837 { desc => 'notify value', type => 'string' },
4838 { desc => 'notify_type', type => 'string' }
4841 desc => 'Hold objects with transits attached, event on error',
4846 sub retrieve_holds_by_usr_notify_value_staff {
4848 my($self, $conn, $auth, $usr_id, $contact, $cType) = @_;
4850 my $e = new_editor(authtoken=>$auth);
4851 $e->checkauth or return $e->event;
4853 if ($e->requestor->id != $usr_id){
4854 $e->allowed('VIEW_HOLD') or return $e->event;
4858 "select" => { "ahr" => ["id", "sms_notify", "phone_notify", "email_notify", "sms_carrier"]},
4862 "capture_time" => undef,
4863 "cancel_time" => undef,
4864 "fulfillment_time" => undef,
4868 if ($cType eq "day_phone" or $cType eq "evening_phone" or
4869 $cType eq "other_phone" or $cType eq "default_phone"){
4870 $q->{where}->{"-not"} = [
4871 { "phone_notify" => { "=" => $contact} },
4872 { "phone_notify" => { "<>" => undef } }
4877 if ($cType eq "default_sms") {
4878 $q->{where}->{"-not"} = [
4879 { "sms_notify" => { "=" => $contact} },
4880 { "sms_notify" => { "<>" => undef } }
4884 if ($cType eq "default_sms_carrier_id") {
4885 $q->{where}->{"-not"} = [
4886 { "sms_carrier" => { "=" => int($contact)} },
4887 { "sms_carrier" => { "<>" => undef } }
4891 if ($cType =~ /notify/){
4892 # this is was notification pref change
4893 # we find all unfulfilled holds that match have that pref
4894 my $optr = $contact == 1 ? "<>" : "="; # unless it's email, true val means we want to query for not null
4895 my $conj = $optr eq '=' ? '-or' : '-and';
4896 if ($cType =~ /sms/) {
4897 $q->{where}->{$conj} = [ { sms_notify => { $optr => undef } }, { sms_notify => { $optr => '' } } ];
4899 if ($cType =~ /phone/) {
4900 $q->{where}->{$conj} = [ { phone_notify => { $optr => undef } }, { phone_notify => { $optr => '' } } ];
4902 if ($cType =~ /email/) {
4904 $q->{where}->{'+ahr'} = 'email_notify';
4906 $q->{where}->{'-not'} = {'+ahr' => 'email_notify'};
4911 my $holds = $e->json_query($q);
4912 #$hold_ids = [ map { $_->{id} } @$hold_ids ];
4917 __PACKAGE__->register_method(
4918 method => "batch_update_holds_by_value_staff",
4919 api_name => "open-ils.circ.holds.batch_update_holds_by_notify_staff",
4921 desc => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
4923 { desc => 'Authentication token', type => 'string' },
4924 { desc => 'User ID', type => 'number' },
4925 { desc => 'Hold IDs', type => 'array' },
4926 { desc => 'old notify value', type => 'string' },
4927 { desc => 'new notify value', type => 'string' },
4928 { desc => 'field name', type => 'string' },
4929 { desc => 'SMS carrier ID', type => 'number' }
4933 desc => 'Hold objects with transits attached, event on error',
4938 sub batch_update_holds_by_value_staff {
4939 my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType, $carrierId) = @_;
4941 my $e = new_editor(authtoken=>$auth, xact=>1);
4942 $e->checkauth or return $e->event;
4943 if ($e->requestor->id != $usr_id){
4944 $e->allowed('UPDATE_HOLD') or return $e->event;
4948 for my $id (@$hold_ids) {
4950 my $hold = $e->retrieve_action_hold_request($id);
4952 if ($cType eq "day_phone" or $cType eq "evening_phone" or
4953 $cType eq "other_phone" or $cType eq "default_phone") {
4955 if ($newval eq '') {
4956 $hold->clear_phone_notify();
4959 $hold->phone_notify($newval);
4963 if ($cType eq "default_sms"){
4964 if ($newval eq '') {
4965 $hold->clear_sms_notify();
4966 $hold->clear_sms_carrier(); # TODO: prevent orphan sms_carrier, via db trigger
4969 $hold->sms_notify($newval);
4970 $hold->sms_carrier($carrierId);
4975 if ($cType eq "default_sms_carrier_id") {
4976 $hold->sms_carrier($newval);
4979 if ($cType =~ /notify/){
4980 # this is a notification pref change
4981 if ($cType =~ /email/) { $hold->email_notify($newval); }
4982 if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
4983 if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
4984 # the other case, where x_notify is changed to true,
4985 # is covered by an actual value being assigned
4988 $e->update_action_hold_request($hold) or return $e->die_event;
4993 $e->commit; #unless $U->event_code($res);
4999 __PACKAGE__->register_method(
5000 method => "retrieve_holds_by_usr_with_notify",
5001 api_name => "open-ils.circ.holds.retrieve.by_usr.with_notify",
5003 desc => "Retrieve the hold, for the specified user using the notify value. $ses_is_req_note",
5005 { desc => 'Authentication token', type => 'string' },
5006 { desc => 'User ID', type => 'number' },
5009 desc => 'Lists of holds with notification values, event on error',
5014 sub retrieve_holds_by_usr_with_notify {
5016 my($self, $conn, $auth, $usr_id) = @_;
5018 my $e = new_editor(authtoken=>$auth);
5019 $e->checkauth or return $e->event;
5021 if ($e->requestor->id != $usr_id){
5022 $e->allowed('VIEW_HOLD') or return $e->event;
5026 "select" => { "ahr" => ["id", "phone_notify", "email_notify", "sms_carrier", "sms_notify"]},
5030 "capture_time" => undef,
5031 "cancel_time" => undef,
5032 "fulfillment_time" => undef,
5036 my $holds = $e->json_query($q);
5040 __PACKAGE__->register_method(
5041 method => "batch_update_holds_by_value",
5042 api_name => "open-ils.circ.holds.batch_update_holds_by_notify",
5044 desc => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
5046 { desc => 'Authentication token', type => 'string' },
5047 { desc => 'User ID', type => 'number' },
5048 { desc => 'Hold IDs', type => 'array' },
5049 { desc => 'old notify value', type => 'string' },
5050 { desc => 'new notify value', type => 'string' },
5051 { desc => 'notify_type', type => 'string' }
5054 desc => 'Hold objects with transits attached, event on error',
5059 sub batch_update_holds_by_value {
5060 my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType) = @_;
5062 my $e = new_editor(authtoken=>$auth, xact=>1);
5063 $e->checkauth or return $e->event;
5064 if ($e->requestor->id != $usr_id){
5065 $e->allowed('UPDATE_HOLD') or return $e->event;
5069 for my $id (@$hold_ids) {
5071 my $hold = $e->retrieve_action_hold_request(int($id));
5073 if ($cType eq "day_phone" or $cType eq "evening_phone" or
5074 $cType eq "other_phone" or $cType eq "default_phone") {
5075 # change phone number value on hold
5076 $hold->phone_notify($newval);
5078 if ($cType eq "default_sms") {
5079 # change SMS number value on hold
5080 $hold->sms_notify($newval);
5083 if ($cType eq "default_sms_carrier_id") {
5084 $hold->sms_carrier(int($newval));
5087 if ($cType =~ /notify/){
5088 # this is a notification pref change
5089 if ($cType =~ /email/) { $hold->email_notify($newval); }
5090 if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
5091 if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
5092 # the other case, where x_notify is changed to true,
5093 # is covered by an actual value being assigned
5096 $e->update_action_hold_request($hold) or return $e->die_event;
5101 $e->commit; #unless $U->event_code($res);
5105 __PACKAGE__->register_method(
5106 method => "hold_metadata",
5107 api_name => "open-ils.circ.hold.get_metadata",
5112 Returns a stream of objects containing whatever bib,
5113 volume, etc. data is available to the specific hold
5117 {desc => 'Hold Type', type => 'string'},
5118 {desc => 'Hold Target(s)', type => 'number or array'},
5119 {desc => 'Context org unit (optional)', type => 'number'}
5123 Stream of hold metadata objects.
5132 my ($self, $client, $hold_type, $hold_targets, $org_id) = @_;
5134 $hold_targets = [$hold_targets] unless ref $hold_targets;
5136 my $e = new_editor();
5137 for my $target (@$hold_targets) {
5139 # create a dummy hold for find_hold_mvr
5140 my $hold = Fieldmapper::action::hold_request->new;
5141 $hold->hold_type($hold_type);
5142 $hold->target($target);
5144 my (undef, $volume, $copy, $issuance, $part, $bre, $metarecord) =
5145 find_hold_mvr($e, $hold, {suppress_mvr => 1});
5147 $bre->clear_marc; # avoid bulk
5153 issuance => $issuance,
5157 metarecord => $metarecord,
5158 metarecord_filters => {}
5161 # If this is a bib hold or metarecord hold, also return the
5162 # available set of MR filters (AKA "Holdable Formats") for the
5163 # hold. For bib holds these may be used to upgrade the hold
5164 # from a bib to metarecord hold.
5165 if ($hold_type eq 'T') {
5166 my $map = $e->search_metabib_metarecord_source_map(
5167 {source => $meta->{bibrecord}->id})->[0];
5170 $meta->{metarecord} =
5171 $e->retrieve_metabib_metarecord($map->metarecord);
5174 # Also fetch the available parts for bib-level holds.
5175 $meta->{parts} = $e->search_biblio_monograph_part(
5177 {record => $bre->id, deleted => 'f'},
5178 {order_by => {bmp => 'label_sortkey'}}
5183 if ($meta->{metarecord}) {
5186 $self->method_lookup('open-ils.circ.mmr.holds.filters')
5187 ->run($meta->{metarecord}->id, $org_id);
5190 $meta->{metarecord_filters} = $filters->{metarecord};
5194 $client->respond($meta);