1 # ---------------------------------------------------------------
2 # Copyright (C) 2005 Georgia Public Library Service
3 # Bill Erickson <highfalutin@gmail.com>
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 # ---------------------------------------------------------------
17 package OpenILS::Application::Circ::Holds;
18 use base qw/OpenILS::Application/;
19 use strict; use warnings;
20 use List::Util qw(shuffle);
21 use OpenILS::Application::AppUtils;
24 use OpenSRF::EX qw(:try);
28 use OpenSRF::Utils::Logger qw(:logger);
29 use OpenILS::Utils::CStoreEditor q/:funcs/;
30 use OpenILS::Utils::PermitHold;
31 use OpenSRF::Utils::SettingsClient;
32 use OpenILS::Const qw/:const/;
33 use OpenILS::Application::Circ::Transit;
34 use OpenILS::Application::Actor::Friends;
36 use DateTime::Format::ISO8601;
37 use OpenILS::Utils::DateTime qw/:datetime/;
38 use Digest::MD5 qw(md5_hex);
39 use OpenSRF::Utils::Cache;
40 use OpenSRF::Utils::JSON;
41 my $apputils = "OpenILS::Application::AppUtils";
44 __PACKAGE__->register_method(
45 method => "test_and_create_hold_batch",
46 api_name => "open-ils.circ.holds.test_and_create.batch",
49 desc => q/This is for batch creating a set of holds where every field is identical except for the targets./,
51 { desc => 'Authentication token', type => 'string' },
52 { desc => 'Hash of named parameters. Same as for open-ils.circ.title_hold.is_possible, though the pertinent target field is automatically populated based on the hold_type and the specified list of targets.', type => 'object'},
53 { desc => 'Array of target ids', type => 'array' }
56 desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
61 __PACKAGE__->register_method(
62 method => "test_and_create_hold_batch",
63 api_name => "open-ils.circ.holds.test_and_create.batch.override",
66 desc => '@see open-ils.circ.holds.test_and_create.batch',
71 sub test_and_create_hold_batch {
72 my( $self, $conn, $auth, $params, $target_list, $oargs ) = @_;
75 if ($self->api_name =~ /override/) {
77 $oargs = { all => 1 } unless defined $oargs;
78 $$params{oargs} = $oargs; # for is_possible checking.
81 my $e = new_editor(authtoken=>$auth);
82 return $e->die_event unless $e->checkauth;
83 $$params{'requestor'} = $e->requestor->id;
86 if ($$params{'hold_type'} eq 'T') { $target_field = 'titleid'; }
87 elsif ($$params{'hold_type'} eq 'C') { $target_field = 'copy_id'; }
88 elsif ($$params{'hold_type'} eq 'R') { $target_field = 'copy_id'; }
89 elsif ($$params{'hold_type'} eq 'F') { $target_field = 'copy_id'; }
90 elsif ($$params{'hold_type'} eq 'I') { $target_field = 'issuanceid'; }
91 elsif ($$params{'hold_type'} eq 'V') { $target_field = 'volume_id'; }
92 elsif ($$params{'hold_type'} eq 'M') { $target_field = 'mrid'; }
93 elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
94 else { return undef; }
96 my $formats_map = delete $$params{holdable_formats_map};
98 foreach (@$target_list) {
99 $$params{$target_field} = $_;
101 # copy the requested formats from the target->formats map
102 # into the top-level formats attr for each hold
103 $$params{holdable_formats} = $formats_map->{$_};
106 ($res) = $self->method_lookup(
107 'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
108 if ($res->{'success'} == 1 || ($override && $res->{place_unfillable})) {
110 $params->{'depth'} = $res->{'depth'} if $res->{'depth'};
112 if ($$oargs{honor_user_settings}) {
113 my $recipient = $e->retrieve_actor_user($$params{patronid})
114 or return $e->die_event;
115 my $opac_hold_notify = $e->search_actor_user_setting(
116 {usr => $$params{patronid}, name => 'opac.hold_notify'})->[0];
117 if ($opac_hold_notify) {
118 if ($opac_hold_notify->value =~ 'email') {
119 $$params{email_notify} = 1;
121 if ($opac_hold_notify->value =~ 'phone') {
122 my $opac_default_phone = $e->search_actor_user_setting(
123 {usr => $$params{patronid}, name => 'opac.default_phone'})->[0];
124 # FIXME - what's up with the ->value putting quotes around the string?
125 if ($opac_default_phone && $opac_default_phone->value =~ /^"(.*)"$/) {
126 $$params{phone_notify} = $1;
129 if ($opac_hold_notify->value =~ 'sms') {
130 my $opac_default_sms_carrier = $e->search_actor_user_setting(
131 {usr => $$params{patronid}, name => 'opac.default_sms_carrier'})->[0];
132 $$params{sms_carrier} = $opac_default_sms_carrier->value if $opac_default_sms_carrier;
133 my $opac_default_sms_notify = $e->search_actor_user_setting(
134 {usr => $$params{patronid}, name => 'opac.default_sms_notify'})->[0];
135 if ($opac_default_sms_notify && $opac_default_sms_notify->value =~ /^"(.*)"$/) {
136 $$params{sms_notify} = $1;
142 # Remove oargs from params so holds can be created.
143 if ($$params{oargs}) {
144 delete $$params{oargs};
147 my $ahr = construct_hold_request_object($params);
148 my ($res2) = $self->method_lookup(
150 ? 'open-ils.circ.holds.create.override'
151 : 'open-ils.circ.holds.create'
152 )->run($auth, $ahr, $oargs);
154 'target' => $$params{$target_field},
157 $conn->respond($res2);
160 'target' => $$params{$target_field},
163 $conn->respond($res);
169 __PACKAGE__->register_method(
170 method => "test_and_create_batch_hold_event",
171 api_name => "open-ils.circ.holds.test_and_create.subscription_batch",
174 desc => q/This is for batch creating a set of holds where every field is identical except for the target users./,
176 { desc => 'Authentication token', type => 'string' },
177 { desc => 'Hash of named parameters. Same as for open-ils.circ.title_hold.is_possible, though the pertinent target field is automatically populated based on the hold_type and the specified list of target users.', type => 'object'},
178 { desc => 'Container ID of the user bucket holding target users', type => 'number' },
179 { desc => 'target object ID (clarified by hold_type in param hash)', type => 'number' },
180 { desc => 'Randomize flag, set to 0 for "not randomized"', type => 'bool' }
183 desc => 'Stream of objects structured as {total=>X, count=>Y, target=>Z, patronid=>A, result=>$hold_id} on success, -1 on missing arg, event (or stream of events on "result" key of object) on error(s)',
188 __PACKAGE__->register_method(
189 method => "test_and_create_batch_hold_event",
190 api_name => "open-ils.circ.holds.test_and_create.subscription_batch.override",
193 desc => '@see open-ils.circ.holds.test_and_create.subscription_batch',
198 sub test_and_create_batch_hold_event {
199 my( $self, $conn, $auth, $params, $target_bucket, $target_id, $randomize, $oargs ) = @_;
202 $randomize //= 1; # default to random hold creation order
203 $$params{hold_type} //= 'T'; # default to title holds
206 if ($self->api_name =~ /override/) {
208 $oargs = { all => 1 } unless defined $oargs;
209 $$params{oargs} = $oargs; # for is_possible checking.
212 my $e = new_editor(authtoken=>$auth);
213 return $e->die_event unless $e->checkauth;
214 $$params{'requestor'} = $e->requestor->id;
217 my $org = $e->requestor->ws_ou || $e->requestor->home_ou;
218 # the perm locaiton shouldn't really matter here since holds
219 # will exist all over and MANAGE_HOLD_GROUPS should be universal
220 my $evt = $U->check_perms($e->requestor->id, $org, 'MANAGE_HOLD_GROUPS');
223 my $rand_setting = $U->ou_ancestor_setting_value($org, 'holds.subscription.randomize');
224 $randomize = $rand_setting if (defined $rand_setting);
227 if ($$params{'hold_type'} eq 'T') { $target_field = 'titleid'; }
228 elsif ($$params{'hold_type'} eq 'C') { $target_field = 'copy_id'; }
229 elsif ($$params{'hold_type'} eq 'R') { $target_field = 'copy_id'; }
230 elsif ($$params{'hold_type'} eq 'F') { $target_field = 'copy_id'; }
231 elsif ($$params{'hold_type'} eq 'I') { $target_field = 'issuanceid'; }
232 elsif ($$params{'hold_type'} eq 'V') { $target_field = 'volume_id'; }
233 elsif ($$params{'hold_type'} eq 'M') { $target_field = 'mrid'; }
234 elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
235 else { return undef; }
237 # Check for a valid record.
238 # XXX For now, because only title holds are allowed, we'll add only that check.
239 my $target_check = $e->json_query({
240 select => {bre => ['id']},
242 where => {deleted => 'f', id => $target_id}
244 return {error=>'invalid_target'} if (!@$target_check);
246 my $formats_map = delete($$params{holdable_formats_map}) || {};
248 my $target_list = $e->search_container_user_bucket_item({bucket => $target_bucket});
249 @$target_list = shuffle(@$target_list) if $randomize;
251 # Record the request...
253 my $bhe = Fieldmapper::action::batch_hold_event->new;
255 $bhe->staff($e->requestor->id);
256 $bhe->bucket($target_bucket);
257 $bhe->target($target_id);
258 $bhe->hold_type($$params{hold_type});
259 $bhe = $e->create_action_batch_hold_event($bhe) or return $e->die_event;
262 my $total = scalar(@$target_list);
264 $conn->respond({total => $total, count => $count});
266 my $triggers = OpenSRF::AppSession->connect('open-ils.trigger');
267 foreach (@$target_list) {
269 $$params{$target_field} = $target_id;
270 $$params{patronid} = $_->target_user;
272 my $usr = $e->retrieve_actor_user([
281 my $user_setting_map = {
282 map { $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) }
286 $$params{pickup_lib} = $$user_setting_map{'opac.default_pickup_location'} || $usr->home_ou;
288 if ($user_setting_map->{'opac.hold_notify'} =~ /email/) {
289 $$params{email_notify} = 1;
291 delete $$params{email_notify};
294 if ($user_setting_map->{'opac.default_phone'} && $user_setting_map->{'opac.hold_notify'} =~ /phone/) {
295 $$params{phone_notify} = $user_setting_map->{'opac.default_phone'};
297 delete $$params{phone_notify};
300 if ($user_setting_map->{'opac.default_sms_carrier'}
301 && $user_setting_map->{'opac.default_sms_notify'}
302 && $user_setting_map->{'opac.hold_notify'} =~ /sms/) {
303 $$params{sms_carrier} = $user_setting_map->{'opac.default_sms_carrier'};
304 $$params{sms_notify} = $user_setting_map->{'opac.default_sms_notify'};
306 delete $$params{sms_carrier};
307 delete $$params{sms_notify};
310 # copy the requested formats from the target->formats map
311 # into the top-level formats attr for each hold ... empty for now, T holds only
312 $$params{holdable_formats} = $formats_map->{$_};
315 ($res) = $self->method_lookup(
316 'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
317 if ($res->{'success'} == 1) {
319 $params->{'depth'} = $res->{'depth'} if $res->{'depth'};
321 # Remove oargs from params so holds can be created.
322 if ($$params{oargs}) {
323 delete $$params{oargs};
326 my $ahr = construct_hold_request_object($params);
327 my ($res2) = $self->method_lookup(
329 ? 'open-ils.circ.holds.create.override'
330 : 'open-ils.circ.holds.create'
331 )->run($auth, $ahr, $oargs);
333 total => $total, count => $count,
334 'patronid' => $$params{patronid},
335 'target' => $$params{$target_field},
338 $conn->respond($res2);
340 unless (ref($res2->{result})) { # success returns a hold id only
342 my $bhem = Fieldmapper::action::batch_hold_event_map->new;
344 $bhem->batch_hold_event($bhe->id);
345 $bhem->hold($res2->{result});
346 $e->create_action_batch_hold_event_map($bhem) or return $e->die_event;
349 my $hold = $e->retrieve_action_hold_request($bhem->hold);
350 $triggers->request('open-ils.trigger.event.autocreate', 'hold_request.success', $hold, $hold->pickup_lib);
355 total => $total, count => $count,
356 'target' => $$params{$target_field},
357 'failedpatronid' => $$params{patronid},
360 $conn->respond($res);
367 __PACKAGE__->register_method(
368 method => "rollback_batch_hold_event",
369 api_name => "open-ils.circ.holds.rollback.subscription_batch",
372 desc => q/This is for batch creating a set of holds where every field is identical except for the target users./,
374 { desc => 'Authentication token', type => 'string' },
375 { desc => 'Hold Group Event ID to roll back', type => 'number' },
378 desc => 'Stream of objects structured as {total=>X, count=>Y} on success, event on error',
383 sub rollback_batch_hold_event {
384 my( $self, $conn, $auth, $event_id ) = @_;
386 my $e = new_editor(authtoken=>$auth,xact=>1);
387 return $e->die_event unless $e->checkauth;
389 my $org = $e->requestor->ws_ou || $e->requestor->home_ou;
390 my $evt = $U->check_perms($e->requestor->id, $org, 'MANAGE_HOLD_GROUPS');
393 my $batch_event = $e->retrieve_action_batch_hold_event($event_id);
394 my $target_list = $e->search_action_batch_hold_event_map({batch_hold_event => $event_id});
396 my $total = scalar(@$target_list);
398 $conn->respond({total => $total, count => $count});
400 for my $target (@$target_list) {
402 $self->method_lookup('open-ils.circ.hold.cancel')->run($auth, $target->hold, 8);
403 $conn->respond({ total => $total, count => $count });
406 $batch_event->cancelled('now');
407 $e->update_action_batch_hold_event($batch_event);
412 sub construct_hold_request_object {
415 my $ahr = Fieldmapper::action::hold_request->new;
418 foreach my $field (keys %{ $params }) {
419 if ($field eq 'depth') { $ahr->selection_depth($$params{$field}); }
420 elsif ($field eq 'patronid') {
421 $ahr->usr($$params{$field}); }
422 elsif ($field eq 'titleid') { $ahr->target($$params{$field}); }
423 elsif ($field eq 'copy_id') { $ahr->target($$params{$field}); }
424 elsif ($field eq 'issuanceid') { $ahr->target($$params{$field}); }
425 elsif ($field eq 'volume_id') { $ahr->target($$params{$field}); }
426 elsif ($field eq 'mrid') { $ahr->target($$params{$field}); }
427 elsif ($field eq 'partid') { $ahr->target($$params{$field}); }
429 $ahr->$field($$params{$field});
435 __PACKAGE__->register_method(
436 method => "create_hold_batch",
437 api_name => "open-ils.circ.holds.create.batch",
440 desc => q/@see open-ils.circ.holds.create.batch/,
442 { desc => 'Authentication token', type => 'string' },
443 { desc => 'Array of hold objects', type => 'array' }
446 desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
451 __PACKAGE__->register_method(
452 method => "create_hold_batch",
453 api_name => "open-ils.circ.holds.create.override.batch",
456 desc => '@see open-ils.circ.holds.create.batch',
461 sub create_hold_batch {
462 my( $self, $conn, $auth, $hold_list, $oargs ) = @_;
463 (my $method = $self->api_name) =~ s/\.batch//og;
464 foreach (@$hold_list) {
465 my ($res) = $self->method_lookup($method)->run($auth, $_, $oargs);
466 $conn->respond($res);
472 __PACKAGE__->register_method(
473 method => "create_hold",
474 api_name => "open-ils.circ.holds.create",
476 desc => "Create a new hold for an item. From a permissions perspective, " .
477 "the login session is used as the 'requestor' of the hold. " .
478 "The hold recipient is determined by the 'usr' setting within the hold object. " .
479 'First we verify the requestor has holds request permissions. ' .
480 'Then we verify that the recipient is allowed to make the given hold. ' .
481 'If not, we see if the requestor has "override" capabilities. If not, ' .
482 'a permission exception is returned. If permissions allow, we cycle ' .
483 'through the set of holds objects and create. ' .
484 'If the recipient does not have permission to place multiple holds ' .
485 'on a single title and said operation is attempted, a permission ' .
486 'exception is returned',
488 { desc => 'Authentication token', type => 'string' },
489 { desc => 'Hold object for hold to be created',
490 type => 'object', class => 'ahr' }
493 desc => 'New ahr ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
498 __PACKAGE__->register_method(
499 method => "create_hold",
500 api_name => "open-ils.circ.holds.create.override",
501 notes => '@see open-ils.circ.holds.create',
503 desc => "If the recipient is not allowed to receive the requested hold, " .
504 "call this method to attempt the override",
506 { desc => 'Authentication token', type => 'string' },
508 desc => 'Hold object for hold to be created',
509 type => 'object', class => 'ahr'
513 desc => 'New hold (ahr) ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
519 my( $self, $conn, $auth, $hold, $oargs ) = @_;
520 return -1 unless $hold;
521 my $e = new_editor(authtoken=>$auth, xact=>1);
522 return $e->die_event unless $e->checkauth;
525 if ($self->api_name =~ /override/) {
527 $oargs = { all => 1 } unless defined $oargs;
532 my $requestor = $e->requestor;
533 my $recipient = $requestor;
535 if( $requestor->id ne $hold->usr ) {
536 # Make sure the requestor is allowed to place holds for
537 # the recipient if they are not the same people
538 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->die_event;
539 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->die_event;
542 # If the related org setting tells us to, block if patron privs have expired
543 my $expire_setting = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_BLOCK_HOLD_FOR_EXPIRED_PATRON);
544 if ($expire_setting) {
545 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
546 clean_ISO8601($recipient->expire_date));
548 push( @events, OpenILS::Event->new(
549 'PATRON_ACCOUNT_EXPIRED',
550 "payload" => {"fail_part" => "actor.usr.privs_expired"}
551 )) if( CORE::time > $expire->epoch ) ;
554 # Now make sure the recipient is allowed to receive the specified hold
555 my $porg = $recipient->home_ou;
556 my $rid = $e->requestor->id;
557 my $t = $hold->hold_type;
559 # See if a duplicate hold already exists
561 usr => $recipient->id,
563 fulfillment_time => undef,
564 target => $hold->target,
565 cancel_time => undef,
568 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
570 my $existing = $e->search_action_hold_request($sargs);
572 # See if the requestor has the CREATE_DUPLICATE_HOLDS perm.
573 my $can_dup = $e->allowed('CREATE_DUPLICATE_HOLDS', $recipient->home_ou);
574 # How many are allowed.
575 my $num_dups = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_MAX_DUPLICATE_HOLDS, $e) || 0;
576 push( @events, OpenILS::Event->new('HOLD_EXISTS'))
577 unless (($t eq 'T' || $t eq 'M') && $can_dup && scalar(@$existing) < $num_dups);
578 # Note: We check for @$existing < $num_dups because we're adding a hold with this call.
581 my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
582 push( @events, OpenILS::Event->new('HOLD_ITEM_CHECKED_OUT')) if $checked_out;
584 if ( $t eq OILS_HOLD_TYPE_METARECORD ) {
585 return $e->die_event unless $e->allowed('MR_HOLDS', $porg);
586 } elsif ( $t eq OILS_HOLD_TYPE_TITLE ) {
587 return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
588 } elsif ( $t eq OILS_HOLD_TYPE_VOLUME ) {
589 return $e->die_event unless $e->allowed('VOLUME_HOLDS', $porg);
590 } elsif ( $t eq OILS_HOLD_TYPE_MONOPART ) {
591 return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
592 } elsif ( $t eq OILS_HOLD_TYPE_ISSUANCE ) {
593 return $e->die_event unless $e->allowed('ISSUANCE_HOLDS', $porg);
594 } elsif ( $t eq OILS_HOLD_TYPE_COPY ) {
595 return $e->die_event unless $e->allowed('COPY_HOLDS', $porg);
596 } elsif ( $t eq OILS_HOLD_TYPE_FORCE || $t eq OILS_HOLD_TYPE_RECALL ) {
597 my $copy = $e->retrieve_asset_copy($hold->target)
598 or return $e->die_event;
599 if ( $t eq OILS_HOLD_TYPE_FORCE ) {
600 return $e->die_event unless $e->allowed('COPY_HOLDS_FORCE', $copy->circ_lib);
601 } elsif ( $t eq OILS_HOLD_TYPE_RECALL ) {
602 return $e->die_event unless $e->allowed('COPY_HOLDS_RECALL', $copy->circ_lib);
611 for my $evt (@events) {
613 my $name = $evt->{textcode};
614 if($oargs->{all} || grep { $_ eq $name } @{$oargs->{events}}) {
615 return $e->die_event unless $e->allowed("$name.override", $porg);
623 # Check for hold expiration in the past, and set it to empty string.
624 $hold->expire_time(undef) if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1);
626 # set the configured expire time
627 unless($hold->expire_time || $U->is_true($hold->frozen)) {
628 $hold->expire_time(calculate_expire_time($recipient->home_ou));
632 # if behind-the-desk pickup is supported at the hold pickup lib,
633 # set the value to the patron default, unless a value has already
634 # been applied. If it's not supported, force the value to false.
636 my $bdous = $U->ou_ancestor_setting_value(
638 'circ.holds.behind_desk_pickup_supported', $e);
641 if (!defined $hold->behind_desk) {
643 my $set = $e->search_actor_user_setting({
645 name => 'circ.holds_behind_desk'
648 $hold->behind_desk('t') if $set and
649 OpenSRF::Utils::JSON->JSON2perl($set->value);
652 # behind the desk not supported, force it to false
653 $hold->behind_desk('f');
656 $hold->requestor($e->requestor->id);
657 $hold->request_lib($e->requestor->ws_ou);
658 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
659 $hold = $e->create_action_hold_request($hold) or return $e->die_event;
663 $conn->respond_complete($hold->id);
665 $U->simplereq('open-ils.hold-targeter',
666 'open-ils.hold-targeter.target', {hold => $hold->id}
667 ) unless $U->is_true($hold->frozen);
672 # makes sure that a user has permission to place the type of requested hold
673 # returns the Perm exception if not allowed, returns undef if all is well
674 sub _check_holds_perm {
675 my($type, $user_id, $org_id) = @_;
679 $evt = $apputils->check_perms($user_id, $org_id, "MR_HOLDS" );
680 } elsif ($type eq "T") {
681 $evt = $apputils->check_perms($user_id, $org_id, "TITLE_HOLDS" );
682 } elsif($type eq "V") {
683 $evt = $apputils->check_perms($user_id, $org_id, "VOLUME_HOLDS");
684 } elsif($type eq "C") {
685 $evt = $apputils->check_perms($user_id, $org_id, "COPY_HOLDS" );
692 # tests if the given user is allowed to place holds on another's behalf
693 sub _check_request_holds_perm {
696 if (my $evt = $apputils->check_perms(
697 $user_id, $org_id, "REQUEST_HOLDS")) {
702 my $ses_is_req_note = 'The login session is the requestor. If the requestor is different from the user, ' .
703 'then the requestor must have VIEW_HOLD permissions';
705 __PACKAGE__->register_method(
706 method => "retrieve_holds_by_id",
707 api_name => "open-ils.circ.holds.retrieve_by_id",
709 desc => "Retrieve the hold, with hold transits attached, for the specified ID. $ses_is_req_note",
711 { desc => 'Authentication token', type => 'string' },
712 { desc => 'Hold ID', type => 'number' }
715 desc => 'Hold object with transits attached, event on error',
721 sub retrieve_holds_by_id {
722 my($self, $client, $auth, $hold_id) = @_;
723 my $e = new_editor(authtoken=>$auth);
724 $e->checkauth or return $e->event;
725 $e->allowed('VIEW_HOLD') or return $e->event;
727 my $holds = $e->search_action_hold_request(
729 { id => $hold_id , fulfillment_time => undef },
731 order_by => { ahr => "request_time" },
733 flesh_fields => {ahr => ['notes']}
738 flesh_hold_transits($holds);
739 flesh_hold_notices($holds, $e);
744 __PACKAGE__->register_method(
745 method => "retrieve_holds",
746 api_name => "open-ils.circ.holds.retrieve",
748 desc => "Retrieves all the holds, with hold transits attached, for the specified user. $ses_is_req_note",
750 { desc => 'Authentication token', type => 'string' },
751 { desc => 'User ID', type => 'integer' },
752 { desc => 'Available Only', type => 'boolean' }
755 desc => 'list of holds, event on error',
760 __PACKAGE__->register_method(
761 method => "retrieve_holds",
762 api_name => "open-ils.circ.holds.id_list.retrieve",
765 desc => "Retrieves all the hold IDs, for the specified user. $ses_is_req_note",
767 { desc => 'Authentication token', type => 'string' },
768 { desc => 'User ID', type => 'integer' },
769 { desc => 'Available Only', type => 'boolean' }
772 desc => 'list of holds, event on error',
777 __PACKAGE__->register_method(
778 method => "retrieve_holds",
779 api_name => "open-ils.circ.holds.canceled.retrieve",
782 desc => "Retrieves all the cancelled holds for the specified user. $ses_is_req_note",
784 { desc => 'Authentication token', type => 'string' },
785 { desc => 'User ID', type => 'integer' }
788 desc => 'list of holds, event on error',
793 __PACKAGE__->register_method(
794 method => "retrieve_holds",
795 api_name => "open-ils.circ.holds.canceled.id_list.retrieve",
798 desc => "Retrieves list of cancelled hold IDs for the specified user. $ses_is_req_note",
800 { desc => 'Authentication token', type => 'string' },
801 { desc => 'User ID', type => 'integer' }
804 desc => 'list of hold IDs, event on error',
811 my ($self, $client, $auth, $user_id, $available) = @_;
813 my $e = new_editor(authtoken=>$auth);
814 return $e->event unless $e->checkauth;
815 $user_id = $e->requestor->id unless defined $user_id;
817 my $notes_filter = {staff => 'f'};
818 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
819 unless($user_id == $e->requestor->id) {
820 if($e->allowed('VIEW_HOLD', $user->home_ou)) {
821 $notes_filter = {staff => 't'}
823 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
824 $e, $user_id, $e->requestor->id, 'hold.view');
825 return $e->event unless $allowed;
828 # staff member looking at his/her own holds can see staff and non-staff notes
829 $notes_filter = {} if $e->allowed('VIEW_HOLD', $user->home_ou);
833 select => {ahr => ['id']},
835 where => {usr => $user_id, fulfillment_time => undef}
838 if($self->api_name =~ /canceled/) {
840 $holds_query->{order_by} =
841 [{class => 'ahr', field => 'cancel_time', direction => 'desc'}];
843 recently_canceled_holds_filter($e, $holds_query);
847 # order non-cancelled holds by ready-for-pickup, then active, followed by suspended
848 # "compare" sorts false values to the front. testing pickup_lib != current_shelf_lib
849 # will sort by pl = csl > pl != csl > followed by csl is null;
850 $holds_query->{order_by} = [
852 field => 'pickup_lib',
853 compare => {'!=' => {'+ahr' => 'current_shelf_lib'}}},
854 {class => 'ahr', field => 'shelf_time'},
855 {class => 'ahr', field => 'frozen'},
856 {class => 'ahr', field => 'request_time'}
859 $holds_query->{where}->{cancel_time} = undef;
861 $holds_query->{where}->{shelf_time} = {'!=' => undef};
863 $holds_query->{where}->{pickup_lib} = {'=' => {'+ahr' => 'current_shelf_lib'}};
867 my $hold_ids = $e->json_query($holds_query);
868 $hold_ids = [ map { $_->{id} } @$hold_ids ];
870 return $hold_ids if $self->api_name =~ /id_list/;
873 for my $hold_id ( @$hold_ids ) {
875 my $hold = $e->retrieve_action_hold_request($hold_id);
876 $hold->notes($e->search_action_hold_request_note({hold => $hold_id, %$notes_filter}));
879 $e->search_action_hold_transit_copy([
880 {hold => $hold->id, cancel_time => undef},
881 {order_by => {ahtc => 'source_send_time desc'}, limit => 1}])->[0]
891 # Creates / augments a set of query filters to search for canceled holds
892 # based on circ.holds.canceled.* org settings.
893 sub recently_canceled_holds_filter {
894 my ($e, $filters) = @_;
896 $filters->{where} ||= {};
899 my $cancel_count = $U->ou_ancestor_setting_value(
900 $e->requestor->ws_ou, 'circ.holds.canceled.display_count', $e);
902 unless($cancel_count) {
903 $cancel_age = $U->ou_ancestor_setting_value(
904 $e->requestor->ws_ou, 'circ.holds.canceled.display_age', $e);
906 # if no settings are defined, default to last 10 cancelled holds
907 $cancel_count = 10 unless $cancel_age;
910 if($cancel_count) { # limit by count
912 $filters->{where}->{cancel_time} = {'!=' => undef};
913 $filters->{limit} = $cancel_count;
915 } elsif($cancel_age) { # limit by age
917 # find all of the canceled holds that were canceled within the configured time frame
918 my $date = DateTime->now->subtract(seconds =>
919 OpenILS::Utils::DateTime->interval_to_seconds($cancel_age));
921 $date = $U->epoch2ISO8601($date->epoch);
922 $filters->{where}->{cancel_time} = {'>=' => $date};
930 __PACKAGE__->register_method(
931 method => 'user_hold_count',
932 api_name => 'open-ils.circ.hold.user.count'
935 sub user_hold_count {
936 my ( $self, $conn, $auth, $userid ) = @_;
937 my $e = new_editor( authtoken => $auth );
938 return $e->event unless $e->checkauth;
939 my $patron = $e->retrieve_actor_user($userid)
941 return $e->event unless $e->allowed( 'VIEW_HOLD', $patron->home_ou );
942 return __user_hold_count( $self, $e, $userid );
945 sub __user_hold_count {
946 my ( $self, $e, $userid ) = @_;
947 my $holds = $e->search_action_hold_request(
950 fulfillment_time => undef,
951 cancel_time => undef,
956 return scalar(@$holds);
960 __PACKAGE__->register_method(
961 method => "retrieve_holds_by_pickup_lib",
962 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
964 "Retrieves all the holds, with hold transits attached, for the specified pickup_ou id."
967 __PACKAGE__->register_method(
968 method => "retrieve_holds_by_pickup_lib",
969 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
970 notes => "Retrieves all the hold ids for the specified pickup_ou id. "
973 sub retrieve_holds_by_pickup_lib {
974 my ($self, $client, $login_session, $ou_id) = @_;
976 #FIXME -- put an appropriate permission check here
977 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
978 # $login_session, $user_id, 'VIEW_HOLD' );
979 #return $evt if $evt;
981 my $holds = $apputils->simplereq(
983 "open-ils.cstore.direct.action.hold_request.search.atomic",
985 pickup_lib => $ou_id ,
986 fulfillment_time => undef,
989 { order_by => { ahr => "request_time" } }
992 if ( ! $self->api_name =~ /id_list/ ) {
993 flesh_hold_transits($holds);
997 return [ map { $_->id } @$holds ];
1001 __PACKAGE__->register_method(
1002 method => "uncancel_hold",
1003 api_name => "open-ils.circ.hold.uncancel"
1007 my($self, $client, $auth, $hold_id) = @_;
1008 my $e = new_editor(authtoken=>$auth, xact=>1);
1009 return $e->die_event unless $e->checkauth;
1011 my $hold = $e->retrieve_action_hold_request($hold_id)
1012 or return $e->die_event;
1013 return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
1015 if ($hold->fulfillment_time) {
1019 unless ($hold->cancel_time) {
1024 # if configured to reset the request time, also reset the expire time
1025 if($U->ou_ancestor_setting_value(
1026 $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
1028 $hold->request_time('now');
1029 $hold->expire_time(calculate_expire_time($hold->request_lib));
1032 $hold->clear_cancel_time;
1033 $hold->clear_cancel_cause;
1034 $hold->clear_cancel_note;
1035 $hold->clear_shelf_time;
1036 $hold->clear_current_copy;
1037 $hold->clear_capture_time;
1038 $hold->clear_prev_check_time;
1039 $hold->clear_shelf_expire_time;
1040 $hold->clear_current_shelf_lib;
1042 $e->update_action_hold_request($hold) or return $e->die_event;
1045 $U->simplereq('open-ils.hold-targeter',
1046 'open-ils.hold-targeter.target', {hold => $hold_id});
1052 __PACKAGE__->register_method(
1053 method => "cancel_hold",
1054 api_name => "open-ils.circ.hold.cancel",
1056 desc => 'Cancels the specified hold. The login session is the requestor. If the requestor is different from the usr field ' .
1057 'on the hold, the requestor must have CANCEL_HOLDS permissions. The hold may be either the hold object or the hold id',
1059 {desc => 'Authentication token', type => 'string'},
1060 {desc => 'Hold ID', type => 'number'},
1061 {desc => 'Cause of Cancellation', type => 'string'},
1062 {desc => 'Note', type => 'string'}
1065 desc => '1 on success, event on error'
1071 my($self, $client, $auth, $holdid, $cause, $note) = @_;
1073 my $e = new_editor(authtoken=>$auth, xact=>1);
1074 return $e->die_event unless $e->checkauth;
1076 my $hold = $e->retrieve_action_hold_request($holdid)
1077 or return $e->die_event;
1079 if( $e->requestor->id ne $hold->usr ) {
1080 return $e->die_event unless $e->allowed('CANCEL_HOLDS');
1083 if ($hold->cancel_time) {
1088 # If the hold is captured, reset the copy status
1089 if( $hold->capture_time and $hold->current_copy ) {
1091 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1092 or return $e->die_event;
1094 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1095 $logger->info("canceling hold $holdid whose item is on the holds shelf");
1096 # $logger->info("setting copy to status 'reshelving' on hold cancel");
1097 # $copy->status(OILS_COPY_STATUS_RESHELVING);
1098 # $copy->editor($e->requestor->id);
1099 # $copy->edit_date('now');
1100 # $e->update_asset_copy($copy) or return $e->event;
1102 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1104 my $hid = $hold->id;
1105 $logger->warn("! canceling hold [$hid] that is in transit");
1106 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
1109 my $trans = $e->retrieve_action_transit_copy($transid);
1110 # Leave the transit alive, but set the copy status to
1111 # reshelving so it will be properly reshelved when it gets back home
1113 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
1114 $e->update_action_transit_copy($trans) or return $e->die_event;
1120 $hold->cancel_time('now');
1121 $hold->cancel_cause($cause);
1122 $hold->cancel_note($note);
1123 $e->update_action_hold_request($hold)
1124 or return $e->die_event;
1128 # re-fetch the hold to pick up the real cancel_time (not "now") for A/T
1130 $hold = $e->retrieve_action_hold_request($hold->id) or return $e->die_event;
1133 if ($e->requestor->id == $hold->usr) {
1134 $U->create_events_for_hook('hold_request.cancel.patron', $hold, $hold->pickup_lib);
1136 $U->create_events_for_hook('hold_request.cancel.staff', $hold, $hold->pickup_lib);
1142 my $update_hold_desc = 'The login session is the requestor. ' .
1143 'If the requestor is different from the usr field on the hold, ' .
1144 'the requestor must have UPDATE_HOLDS permissions. ' .
1145 'If supplying a hash of hold data, "id" must be included. ' .
1146 'The hash is ignored if a hold object is supplied, ' .
1147 'so you should supply only one kind of hold data argument.' ;
1149 __PACKAGE__->register_method(
1150 method => "update_hold",
1151 api_name => "open-ils.circ.hold.update",
1153 desc => "Updates the specified hold. $update_hold_desc",
1155 {desc => 'Authentication token', type => 'string'},
1156 {desc => 'Hold Object', type => 'object'},
1157 {desc => 'Hash of values to be applied', type => 'object'}
1160 desc => 'Hold ID on success, event on error',
1166 __PACKAGE__->register_method(
1167 method => "batch_update_hold",
1168 api_name => "open-ils.circ.hold.update.batch",
1171 desc => "Updates the specified hold(s). $update_hold_desc",
1173 {desc => 'Authentication token', type => 'string'},
1174 {desc => 'Array of hold obejcts', type => 'array' },
1175 {desc => 'Array of hashes of values to be applied', type => 'array' }
1178 desc => 'Hold ID per success, event per error',
1184 my($self, $client, $auth, $hold, $values) = @_;
1185 my $e = new_editor(authtoken=>$auth, xact=>1);
1186 return $e->die_event unless $e->checkauth;
1187 my $resp = update_hold_impl($self, $e, $hold, $values);
1188 if ($U->event_code($resp)) {
1192 $e->commit; # FIXME: update_hold_impl already does $e->commit ??
1196 sub batch_update_hold {
1197 my($self, $client, $auth, $hold_list, $values_list) = @_;
1198 my $e = new_editor(authtoken=>$auth);
1199 return $e->die_event unless $e->checkauth;
1201 my $count = ($hold_list) ? scalar(@$hold_list) : scalar(@$values_list); # FIXME: we don't know for sure that we got $values_list. we could have neither list.
1203 $values_list ||= []; # FIXME: either move this above $count declaration, or send an event if both lists undef. Probably the latter.
1205 # FIXME: Failing over to [] guarantees warnings for "Use of unitialized value" in update_hold_impl call.
1206 # FIXME: We should be sure we only call update_hold_impl with hold object OR hash, not both.
1208 for my $idx (0..$count-1) {
1210 my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
1211 $e->xact_commit unless $U->event_code($resp);
1212 $client->respond($resp);
1216 return undef; # not in the register return type, assuming we should always have at least one list populated
1219 sub update_hold_impl {
1220 my($self, $e, $hold, $values) = @_;
1222 my $need_retarget = 0;
1225 $hold = $e->retrieve_action_hold_request($values->{id})
1226 or return $e->die_event;
1227 for my $k (keys %$values) {
1228 # Outside of pickup_lib (covered by the first regex) I don't know when these would currently change.
1229 # But hey, why not cover things that may happen later?
1230 if ($k =~ '_(lib|ou)$' || $k eq 'target' || $k eq 'hold_type' || $k eq 'requestor' || $k eq 'selection_depth' || $k eq 'holdable_formats') {
1231 if (defined $values->{$k} && defined $hold->$k() && $values->{$k} ne $hold->$k()) {
1232 # Value changed? RETARGET!
1234 } elsif (defined $hold->$k() != defined $values->{$k}) {
1235 # Value being set or cleared? RETARGET!
1239 if (defined $values->{$k}) {
1240 $hold->$k($values->{$k});
1242 my $f = "clear_$k"; $hold->$f();
1247 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
1248 or return $e->die_event;
1250 # don't allow the user to be changed
1251 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
1253 if($hold->usr ne $e->requestor->id) {
1254 # if the hold is for a different user, make sure the
1255 # requestor has the appropriate permissions
1256 my $usr = $e->retrieve_actor_user($hold->usr)
1257 or return $e->die_event;
1258 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1262 # --------------------------------------------------------------
1263 # Changing the request time is like playing God
1264 # --------------------------------------------------------------
1265 if($hold->request_time ne $orig_hold->request_time) {
1266 return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
1267 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
1271 # --------------------------------------------------------------
1272 # Code for making sure staff have appropriate permissons for cut_in_line
1273 # This, as is, doesn't prevent a user from cutting their own holds in line
1275 # --------------------------------------------------------------
1276 if($U->is_true($hold->cut_in_line) ne $U->is_true($orig_hold->cut_in_line)) {
1277 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
1281 # --------------------------------------------------------------
1282 # Disallow hold suspencion if the hold is already captured.
1283 # --------------------------------------------------------------
1284 if ($U->is_true($hold->frozen) and not $U->is_true($orig_hold->frozen)) {
1285 $hold_status = _hold_status($e, $hold);
1286 if ($hold_status > 2 && $hold_status != 7) { # hold is captured
1287 $logger->info("bypassing hold freeze on captured hold");
1288 return OpenILS::Event->new('HOLD_SUSPEND_AFTER_CAPTURE');
1293 # --------------------------------------------------------------
1294 # if the hold is on the holds shelf or in transit and the pickup
1295 # lib changes we need to create a new transit.
1296 # --------------------------------------------------------------
1297 if($orig_hold->pickup_lib ne $hold->pickup_lib) {
1299 $hold_status = _hold_status($e, $hold) unless $hold_status;
1301 if($hold_status == 3) { # in transit
1303 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
1304 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
1306 $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
1308 # update the transit to reflect the new pickup location
1309 my $transit = $e->search_action_hold_transit_copy(
1310 {hold=>$hold->id, cancel_time => undef, dest_recv_time => undef})->[0]
1311 or return $e->die_event;
1313 $transit->prev_dest($transit->dest); # mark the previous destination on the transit
1314 $transit->dest($hold->pickup_lib);
1315 $e->update_action_hold_transit_copy($transit) or return $e->die_event;
1317 } elsif($hold_status == 4 or $hold_status == 5 or $hold_status == 8) { # on holds shelf
1319 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
1320 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
1322 $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
1324 if ($hold->pickup_lib eq $orig_hold->current_shelf_lib) {
1325 # This can happen if the pickup lib is changed while the hold is
1326 # on the shelf, then changed back to the original pickup lib.
1327 # Restore the original shelf_expire_time to prevent abuse.
1328 set_hold_shelf_expire_time(undef, $hold, $e, $hold->shelf_time);
1331 # clear to prevent premature shelf expiration
1332 $hold->clear_shelf_expire_time;
1334 # If a copy is targeted and pickup lib changes,
1335 # clear the current_copy so a retarget will re-evaluate
1336 # the hold from scratch.
1337 } elsif ($hold_status == 2) {
1338 $logger->info("Pickup location changed and waiting for capture, clear current_copy for hold ".$hold->id);
1339 $hold->clear_current_copy;
1343 if($U->is_true($hold->frozen)) {
1344 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1345 $hold->clear_current_copy;
1346 $hold->clear_prev_check_time;
1347 # Clear expire_time to prevent frozen holds from expiring.
1348 $logger->info("clearing expire_time for frozen hold ".$hold->id);
1349 $hold->clear_expire_time;
1352 # If the hold_expire_time is in the past && is not equal to the
1353 # original expire_time, then reset the expire time to be in the
1355 if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1 && $U->datecmp($hold->expire_time, $orig_hold->expire_time) != 0) {
1356 $hold->expire_time(calculate_expire_time($hold->request_lib));
1359 # If the hold is reactivated, reset the expire_time.
1360 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1361 $logger->info("Reset expire_time on activated hold ".$hold->id);
1362 $hold->expire_time(calculate_expire_time($hold->request_lib));
1365 $e->update_action_hold_request($hold) or return $e->die_event;
1368 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1369 $logger->info("Running targeter on activated hold ".$hold->id);
1370 $U->simplereq('open-ils.hold-targeter',
1371 'open-ils.hold-targeter.target', {hold => $hold->id});
1374 # a change to mint-condition changes the set of potential copies, so retarget the hold;
1375 if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
1376 _reset_hold($self, $e->requestor, $hold)
1377 } elsif($need_retarget && !defined $hold->capture_time()) { # If needed, retarget the hold due to changes
1378 $U->simplereq('open-ils.hold-targeter',
1379 'open-ils.hold-targeter.target', {hold => $hold->id});
1385 # this does not update the hold in the DB. It only
1386 # sets the shelf_expire_time field on the hold object.
1387 # start_time is optional and defaults to 'now'
1388 sub set_hold_shelf_expire_time {
1389 my ($class, $hold, $editor, $start_time) = @_;
1391 my $shelf_expire = $U->ou_ancestor_setting_value(
1393 'circ.holds.default_shelf_expire_interval',
1397 return undef unless $shelf_expire;
1399 $start_time = ($start_time) ?
1400 DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time)) :
1401 DateTime->now(time_zone => 'local'); # without time_zone we get UTC ... yuck!
1403 my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($shelf_expire);
1404 my $expire_time = $start_time->add(seconds => $seconds);
1406 # if the shelf expire time overlaps with a pickup lib's
1407 # closed date, push it out to the first open date
1408 my $dateinfo = $U->storagereq(
1409 'open-ils.storage.actor.org_unit.closed_date.overlap',
1410 $hold->pickup_lib, $expire_time->strftime('%FT%T%z'));
1413 my $dt_parser = DateTime::Format::ISO8601->new;
1414 $expire_time = $dt_parser->parse_datetime(clean_ISO8601($dateinfo->{end}));
1416 # TODO: enable/disable time bump via setting?
1417 $expire_time->set(hour => '23', minute => '59', second => '59');
1419 $logger->info("circulator: shelf_expire_time overlaps".
1420 " with closed date, pushing expire time to $expire_time");
1423 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
1429 my($e, $orig_hold, $hold, $copy) = @_;
1430 my $src = $orig_hold->pickup_lib;
1431 my $dest = $hold->pickup_lib;
1433 $logger->info("putting hold into transit on pickup_lib update");
1435 my $transit = Fieldmapper::action::hold_transit_copy->new;
1436 $transit->hold($hold->id);
1437 $transit->source($src);
1438 $transit->dest($dest);
1439 $transit->target_copy($copy->id);
1440 $transit->source_send_time('now');
1441 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1443 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1444 $copy->editor($e->requestor->id);
1445 $copy->edit_date('now');
1447 $e->create_action_hold_transit_copy($transit) or return $e->die_event;
1448 $e->update_asset_copy($copy) or return $e->die_event;
1452 # if the hold is frozen, this method ensures that the hold is not "targeted",
1453 # that is, it clears the current_copy and prev_check_time to essentiallly
1454 # reset the hold. If it is being activated, it runs the targeter in the background
1455 sub update_hold_if_frozen {
1456 my($self, $e, $hold, $orig_hold) = @_;
1457 return if $hold->capture_time;
1459 if($U->is_true($hold->frozen)) {
1460 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1461 $hold->clear_current_copy;
1462 $hold->clear_prev_check_time;
1465 if($U->is_true($orig_hold->frozen)) {
1466 $logger->info("Running targeter on activated hold ".$hold->id);
1467 $U->simplereq('open-ils.hold-targeter',
1468 'open-ils.hold-targeter.target', {hold => $hold->id});
1473 __PACKAGE__->register_method(
1474 method => "hold_note_CUD",
1475 api_name => "open-ils.circ.hold_request.note.cud",
1477 desc => 'Create, update or delete a hold request note. If the operator (from Auth. token) '
1478 . 'is not the owner of the hold, the UPDATE_HOLD permission is required',
1480 { desc => 'Authentication token', type => 'string' },
1481 { desc => 'Hold note object', type => 'object' }
1484 desc => 'Returns the note ID, event on error'
1490 my($self, $conn, $auth, $note) = @_;
1492 my $e = new_editor(authtoken => $auth, xact => 1);
1493 return $e->die_event unless $e->checkauth;
1495 my $hold = $e->retrieve_action_hold_request($note->hold)
1496 or return $e->die_event;
1498 if($hold->usr ne $e->requestor->id) {
1499 my $usr = $e->retrieve_actor_user($hold->usr);
1500 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1501 $note->staff('t') if $note->isnew;
1505 $e->create_action_hold_request_note($note) or return $e->die_event;
1506 } elsif($note->ischanged) {
1507 $e->update_action_hold_request_note($note) or return $e->die_event;
1508 } elsif($note->isdeleted) {
1509 $e->delete_action_hold_request_note($note) or return $e->die_event;
1517 __PACKAGE__->register_method(
1518 method => "retrieve_hold_status",
1519 api_name => "open-ils.circ.hold.status.retrieve",
1521 desc => 'Calculates the current status of the hold. The requestor must have ' .
1522 'VIEW_HOLD permissions if the hold is for a user other than the requestor' ,
1524 { desc => 'Hold ID', type => 'number' }
1527 # type => 'number', # event sometimes
1528 desc => <<'END_OF_DESC'
1529 Returns event on error or:
1530 -1 on error (for now),
1531 1 for 'waiting for copy to become available',
1532 2 for 'waiting for copy capture',
1535 5 for 'hold-shelf-delay'
1538 8 for 'captured, on wrong hold shelf'
1545 sub retrieve_hold_status {
1546 my($self, $client, $auth, $hold_id) = @_;
1548 my $e = new_editor(authtoken => $auth);
1549 return $e->event unless $e->checkauth;
1550 my $hold = $e->retrieve_action_hold_request($hold_id)
1551 or return $e->event;
1553 if( $e->requestor->id != $hold->usr ) {
1554 return $e->event unless $e->allowed('VIEW_HOLD');
1557 return _hold_status($e, $hold);
1563 if ($hold->cancel_time) {
1566 if ($U->is_true($hold->frozen) && !$hold->capture_time) {
1569 if ($hold->current_shelf_lib and $hold->current_shelf_lib ne $hold->pickup_lib) {
1572 if ($hold->fulfillment_time) {
1575 return 1 unless $hold->current_copy;
1576 return 2 unless $hold->capture_time;
1578 my $copy = $hold->current_copy;
1579 unless( ref $copy ) {
1580 $copy = $e->retrieve_asset_copy($hold->current_copy)
1581 or return $e->event;
1584 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
1586 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
1588 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
1589 return 4 unless $hs_wait_interval;
1591 # if a hold_shelf_status_delay interval is defined and start_time plus
1592 # the interval is greater than now, consider the hold to be in the virtual
1593 # "on its way to the holds shelf" status. Return 5.
1595 my $transit = $e->search_action_hold_transit_copy({
1597 target_copy => $copy->id,
1598 cancel_time => undef,
1599 dest_recv_time => {'!=' => undef},
1601 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
1602 $start_time = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time));
1603 my $end_time = $start_time->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($hs_wait_interval));
1605 return 5 if $end_time > DateTime->now;
1614 __PACKAGE__->register_method(
1615 method => "retrieve_hold_queue_stats",
1616 api_name => "open-ils.circ.hold.queue_stats.retrieve",
1618 desc => 'Returns summary data about the state of a hold',
1620 { desc => 'Authentication token', type => 'string'},
1621 { desc => 'Hold ID', type => 'number'},
1624 desc => q/Summary object with keys:
1625 total_holds : total holds in queue
1626 queue_position : current queue position
1627 potential_copies : number of potential copies for this hold
1628 estimated_wait : estimated wait time in days
1629 status : hold status
1630 -1 => error or unexpected state,
1631 1 => 'waiting for copy to become available',
1632 2 => 'waiting for copy capture',
1635 5 => 'hold-shelf-delay'
1642 sub retrieve_hold_queue_stats {
1643 my($self, $conn, $auth, $hold_id) = @_;
1644 my $e = new_editor(authtoken => $auth);
1645 return $e->event unless $e->checkauth;
1646 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
1647 if($e->requestor->id != $hold->usr) {
1648 return $e->event unless $e->allowed('VIEW_HOLD');
1650 return retrieve_hold_queue_status_impl($e, $hold);
1653 sub retrieve_hold_queue_status_impl {
1657 # The holds queue is defined as the distinct set of holds that share at
1658 # least one potential copy with the context hold, plus any holds that
1659 # share the same hold type and target. The latter part exists to
1660 # accomodate holds that currently have no potential copies
1661 my $q_holds = $e->json_query({
1663 # fetch cut_in_line and request_time since they're in the order_by
1664 # and we're asking for distinct values
1665 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1669 select => { ahcm => ['hold'] },
1674 'field' => 'target_copy',
1675 'fkey' => 'target_copy'
1679 where => { '+ahcm2' => { hold => $hold->id } },
1686 "field" => "cut_in_line",
1687 "transform" => "coalesce",
1689 "direction" => "desc"
1691 { "class" => "ahr", "field" => "request_time" }
1696 if (!@$q_holds) { # none? maybe we don't have a map ...
1697 $q_holds = $e->json_query({
1698 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1703 "field" => "cut_in_line",
1704 "transform" => "coalesce",
1706 "direction" => "desc"
1708 { "class" => "ahr", "field" => "request_time" }
1711 hold_type => $hold->hold_type,
1712 target => $hold->target,
1713 capture_time => undef,
1714 cancel_time => undef,
1716 {expire_time => undef },
1717 {expire_time => {'>' => 'now'}}
1725 for my $h (@$q_holds) {
1726 last if $h->{id} == $hold->id;
1730 my $hold_data = $e->json_query({
1732 acp => [ {column => 'id', transform => 'count', aggregate => 1, alias => 'count'} ],
1733 ccm => [ {column =>'avg_wait_time'} ]
1739 ccm => {type => 'left'}
1744 where => {'+ahcm' => {hold => $hold->id} }
1747 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1749 my $default_wait = $U->ou_ancestor_setting_value(
1750 $user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL, $e);
1751 my $min_wait = $U->ou_ancestor_setting_value(
1752 $user_org, 'circ.holds.min_estimated_wait_interval', $e);
1753 $min_wait = OpenILS::Utils::DateTime->interval_to_seconds($min_wait || '0 seconds');
1754 $default_wait ||= '0 seconds';
1756 # Estimated wait time is the average wait time across the set
1757 # of potential copies, divided by the number of potential copies
1758 # times the queue position.
1760 my $combined_secs = 0;
1761 my $num_potentials = 0;
1763 for my $wait_data (@$hold_data) {
1764 my $count += $wait_data->{count};
1765 $combined_secs += $count *
1766 OpenILS::Utils::DateTime->interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
1767 $num_potentials += $count;
1770 my $estimated_wait = -1;
1772 if($num_potentials) {
1773 my $avg_wait = $combined_secs / $num_potentials;
1774 $estimated_wait = $qpos * ($avg_wait / $num_potentials);
1775 $estimated_wait = $min_wait if $estimated_wait < $min_wait and $estimated_wait != -1;
1779 total_holds => scalar(@$q_holds),
1780 queue_position => $qpos,
1781 potential_copies => $num_potentials,
1782 status => _hold_status( $e, $hold ),
1783 estimated_wait => int($estimated_wait)
1788 sub fetch_open_hold_by_current_copy {
1791 my $hold = $apputils->simplereq(
1793 'open-ils.cstore.direct.action.hold_request.search.atomic',
1794 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1795 return $hold->[0] if ref($hold);
1799 sub fetch_related_holds {
1802 return $apputils->simplereq(
1804 'open-ils.cstore.direct.action.hold_request.search.atomic',
1805 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1809 __PACKAGE__->register_method(
1810 method => "hold_pull_list",
1811 api_name => "open-ils.circ.hold_pull_list.retrieve",
1813 desc => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1814 'The location is determined by the login session.',
1816 { desc => 'Limit (optional)', type => 'number'},
1817 { desc => 'Offset (optional)', type => 'number'},
1820 desc => 'reference to a list of holds, or event on failure',
1825 __PACKAGE__->register_method(
1826 method => "hold_pull_list",
1827 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
1829 desc => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
1830 'The location is determined by the login session.',
1832 { desc => 'Limit (optional)', type => 'number'},
1833 { desc => 'Offset (optional)', type => 'number'},
1836 desc => 'reference to a list of holds, or event on failure',
1841 __PACKAGE__->register_method(
1842 method => "hold_pull_list",
1843 api_name => "open-ils.circ.hold_pull_list.retrieve.count",
1845 desc => 'Returns a count of holds that need to be "pulled" by a given location. ' .
1846 'The location is determined by the login session.',
1848 { desc => 'Limit (optional)', type => 'number'},
1849 { desc => 'Offset (optional)', type => 'number'},
1852 desc => 'Holds count (integer), or event on failure',
1858 __PACKAGE__->register_method(
1859 method => "hold_pull_list",
1861 # TODO: tag with api_level 2 once fully supported
1862 api_name => "open-ils.circ.hold_pull_list.fleshed.stream",
1864 desc => q/Returns a stream of fleshed holds that need to be
1865 "pulled" by a given location. The location is
1866 determined by the login session.
1867 This API calls always run in authoritative mode./,
1869 { desc => 'Limit (optional)', type => 'number'},
1870 { desc => 'Offset (optional)', type => 'number'},
1873 desc => 'Stream of holds holds, or event on failure',
1878 sub hold_pull_list {
1879 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1880 my( $reqr, $evt ) = $U->checkses($authtoken);
1881 return $evt if $evt;
1883 my $org = $reqr->ws_ou || $reqr->home_ou;
1884 # the perm locaiton shouldn't really matter here since holds
1885 # will exist all over and VIEW_HOLDS should be universal
1886 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1887 return $evt if $evt;
1889 if($self->api_name =~ /count/) {
1891 my $count = $U->storagereq(
1892 'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1893 $org, $limit, $offset );
1895 $logger->info("Grabbing pull list for org unit $org with $count items");
1898 } elsif( $self->api_name =~ /id_list/ ) {
1900 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1901 $org, $limit, $offset );
1903 } elsif ($self->api_name =~ /fleshed/) {
1905 my $ids = $U->storagereq(
1906 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1907 $org, $limit, $offset );
1909 my $e = new_editor(xact => 1, requestor => $reqr);
1910 $conn->respond(uber_hold_impl($e, $_, {flesh_acpl => 1})) for @$ids;
1912 $conn->respond_complete;
1917 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1918 $org, $limit, $offset );
1922 __PACKAGE__->register_method(
1923 method => "print_hold_pull_list",
1924 api_name => "open-ils.circ.hold_pull_list.print",
1926 desc => 'Returns an HTML-formatted holds pull list',
1928 { desc => 'Authtoken', type => 'string'},
1929 { desc => 'Org unit ID. Optional, defaults to workstation org unit', type => 'number'},
1932 desc => 'HTML string',
1938 sub print_hold_pull_list {
1939 my($self, $client, $auth, $org_id) = @_;
1941 my $e = new_editor(authtoken=>$auth);
1942 return $e->event unless $e->checkauth;
1944 $org_id = (defined $org_id) ? $org_id : $e->requestor->ws_ou;
1945 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1947 my $hold_ids = $U->storagereq(
1948 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1951 return undef unless @$hold_ids;
1953 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1955 # Holds will /NOT/ be in order after this ...
1956 my $holds = $e->search_action_hold_request({id => $hold_ids}, {substream => 1});
1957 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1959 # ... so we must resort.
1960 my $hold_map = +{map { $_->id => $_ } @$holds};
1961 my $sorted_holds = [];
1962 push @$sorted_holds, $hold_map->{$_} foreach @$hold_ids;
1964 return $U->fire_object_event(
1965 undef, "ahr.format.pull_list", $sorted_holds,
1966 $org_id, undef, undef, $client
1971 __PACKAGE__->register_method(
1972 method => "print_hold_pull_list_stream",
1974 api_name => "open-ils.circ.hold_pull_list.print.stream",
1976 desc => 'Returns a stream of fleshed holds',
1978 { desc => 'Authtoken', type => 'string'},
1979 { desc => 'Hash of optional param: Org unit ID (defaults to workstation org unit), limit, offset, sort (array of: acplo.position, prefix, call_number, suffix, request_time)',
1984 desc => 'A stream of fleshed holds',
1990 sub print_hold_pull_list_stream {
1991 my($self, $client, $auth, $params) = @_;
1993 my $e = new_editor(authtoken=>$auth);
1994 return $e->die_event unless $e->checkauth;
1996 delete($$params{org_id}) unless (int($$params{org_id}));
1997 delete($$params{limit}) unless (int($$params{limit}));
1998 delete($$params{offset}) unless (int($$params{offset}));
1999 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2000 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2001 $$params{chunk_size} ||= 10;
2002 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2004 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2005 return $e->die_event unless $e->allowed('VIEW_HOLD', $$params{org_id });
2008 if ($$params{sort} && @{ $$params{sort} }) {
2009 for my $s (@{ $$params{sort} }) {
2010 if ($s eq 'acplo.position') {
2012 "class" => "acplo", "field" => "position",
2013 "transform" => "coalesce", "params" => [999]
2015 } elsif ($s eq 'prefix') {
2016 push @$sort, {"class" => "acnp", "field" => "label_sortkey"};
2017 } elsif ($s eq 'call_number') {
2018 push @$sort, {"class" => "acn", "field" => "label_sortkey"};
2019 } elsif ($s eq 'suffix') {
2020 push @$sort, {"class" => "acns", "field" => "label_sortkey"};
2021 } elsif ($s eq 'request_time') {
2022 push @$sort, {"class" => "ahr", "field" => "request_time"};
2026 push @$sort, {"class" => "ahr", "field" => "request_time"};
2029 my $holds_ids = $e->json_query(
2031 "select" => {"ahr" => ["id"]},
2036 "fkey" => "current_copy",
2038 "circ_lib" => $$params{org_id}
2043 "fkey" => "call_number",
2057 "fkey" => "circ_lib",
2060 "location" => {"=" => {"+acp" => "location"}}
2068 "is_available" => "t"
2077 "capture_time" => undef,
2078 "cancel_time" => undef,
2080 {"expire_time" => undef },
2081 {"expire_time" => {">" => "now"}}
2085 (@$sort ? (order_by => $sort) : ()),
2086 ($$params{limit} ? (limit => $$params{limit}) : ()),
2087 ($$params{offset} ? (offset => $$params{offset}) : ())
2088 }, {"substream" => 1}
2089 ) or return $e->die_event;
2091 $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
2094 for my $hid (@$holds_ids) {
2095 push @chunk, $e->retrieve_action_hold_request([
2099 "ahr" => ["usr", "current_copy"],
2101 "acp" => ["location", "call_number", "parts"],
2102 "acn" => ["record","prefix","suffix"]
2107 if (@chunk >= $$params{chunk_size}) {
2108 $client->respond( \@chunk );
2112 $client->respond_complete( \@chunk ) if (@chunk);
2119 __PACKAGE__->register_method(
2120 method => 'fetch_hold_notify',
2121 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
2124 Returns a list of hold notification objects based on hold id.
2125 @param authtoken The loggin session key
2126 @param holdid The id of the hold whose notifications we want to retrieve
2127 @return An array of hold notification objects, event on error.
2131 sub fetch_hold_notify {
2132 my( $self, $conn, $authtoken, $holdid ) = @_;
2133 my( $requestor, $evt ) = $U->checkses($authtoken);
2134 return $evt if $evt;
2135 my ($hold, $patron);
2136 ($hold, $evt) = $U->fetch_hold($holdid);
2137 return $evt if $evt;
2138 ($patron, $evt) = $U->fetch_user($hold->usr);
2139 return $evt if $evt;
2141 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
2142 return $evt if $evt;
2144 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
2145 return $U->cstorereq(
2146 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
2150 __PACKAGE__->register_method(
2151 method => 'create_hold_notify',
2152 api_name => 'open-ils.circ.hold_notification.create',
2154 Creates a new hold notification object
2155 @param authtoken The login session key
2156 @param notification The hold notification object to create
2157 @return ID of the new object on success, Event on error
2161 sub create_hold_notify {
2162 my( $self, $conn, $auth, $note ) = @_;
2163 my $e = new_editor(authtoken=>$auth, xact=>1);
2164 return $e->die_event unless $e->checkauth;
2166 my $hold = $e->retrieve_action_hold_request($note->hold)
2167 or return $e->die_event;
2168 my $patron = $e->retrieve_actor_user($hold->usr)
2169 or return $e->die_event;
2171 return $e->die_event unless
2172 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
2174 $note->notify_staff($e->requestor->id);
2175 $e->create_action_hold_notification($note) or return $e->die_event;
2180 __PACKAGE__->register_method(
2181 method => 'create_hold_note',
2182 api_name => 'open-ils.circ.hold_note.create',
2184 Creates a new hold request note object
2185 @param authtoken The login session key
2186 @param note The hold note object to create
2187 @return ID of the new object on success, Event on error
2191 sub create_hold_note {
2192 my( $self, $conn, $auth, $note ) = @_;
2193 my $e = new_editor(authtoken=>$auth, xact=>1);
2194 return $e->die_event unless $e->checkauth;
2196 my $hold = $e->retrieve_action_hold_request($note->hold)
2197 or return $e->die_event;
2198 my $patron = $e->retrieve_actor_user($hold->usr)
2199 or return $e->die_event;
2201 return $e->die_event unless
2202 $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
2204 $e->create_action_hold_request_note($note) or return $e->die_event;
2209 __PACKAGE__->register_method(
2210 method => 'reset_hold',
2211 api_name => 'open-ils.circ.hold.reset',
2213 Un-captures and un-targets a hold, essentially returning
2214 it to the state it was in directly after it was placed,
2215 then attempts to re-target the hold
2216 @param authtoken The login session key
2217 @param holdid The id of the hold
2223 my( $self, $conn, $auth, $holdid ) = @_;
2225 my ($hold, $evt) = $U->fetch_hold($holdid);
2226 return $evt if $evt;
2227 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
2228 return $evt if $evt;
2229 $evt = _reset_hold($self, $reqr, $hold);
2230 return $evt if $evt;
2235 __PACKAGE__->register_method(
2236 method => 'reset_hold_batch',
2237 api_name => 'open-ils.circ.hold.reset.batch'
2240 sub reset_hold_batch {
2241 my($self, $conn, $auth, $hold_ids) = @_;
2243 my $e = new_editor(authtoken => $auth);
2244 return $e->event unless $e->checkauth;
2246 for my $hold_id ($hold_ids) {
2248 my $hold = $e->retrieve_action_hold_request(
2249 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
2250 or return $e->event;
2252 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
2253 _reset_hold($self, $e->requestor, $hold);
2261 my ($self, $reqr, $hold) = @_;
2263 my $e = new_editor(xact =>1, requestor => $reqr);
2265 $logger->info("reseting hold ".$hold->id);
2267 my $hid = $hold->id;
2269 if( $hold->capture_time and $hold->current_copy ) {
2271 my $copy = $e->retrieve_asset_copy($hold->current_copy)
2272 or return $e->die_event;
2274 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2275 $logger->info("setting copy to status 'reshelving' on hold retarget");
2276 $copy->status(OILS_COPY_STATUS_RESHELVING);
2277 $copy->editor($e->requestor->id);
2278 $copy->edit_date('now');
2279 $e->update_asset_copy($copy) or return $e->die_event;
2281 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
2283 $logger->warn("! reseting hold [$hid] that is in transit");
2284 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
2287 my $trans = $e->retrieve_action_transit_copy($transid);
2289 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
2290 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
2291 $logger->info("Transit abort completed with result $evt");
2292 unless ("$evt" eq 1) {
2301 $hold->clear_capture_time;
2302 $hold->clear_current_copy;
2303 $hold->clear_shelf_time;
2304 $hold->clear_shelf_expire_time;
2305 $hold->clear_current_shelf_lib;
2307 $e->update_action_hold_request($hold) or return $e->die_event;
2310 $U->simplereq('open-ils.hold-targeter',
2311 'open-ils.hold-targeter.target', {hold => $hold->id});
2317 __PACKAGE__->register_method(
2318 method => 'fetch_open_title_holds',
2319 api_name => 'open-ils.circ.open_holds.retrieve',
2321 Returns a list ids of un-fulfilled holds for a given title id
2322 @param authtoken The login session key
2323 @param id the id of the item whose holds we want to retrieve
2324 @param type The hold type - M, T, I, V, C, F, R
2328 sub fetch_open_title_holds {
2329 my( $self, $conn, $auth, $id, $type, $org ) = @_;
2330 my $e = new_editor( authtoken => $auth );
2331 return $e->event unless $e->checkauth;
2334 $org ||= $e->requestor->ws_ou;
2336 # return $e->search_action_hold_request(
2337 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
2339 # XXX make me return IDs in the future ^--
2340 my $holds = $e->search_action_hold_request(
2343 cancel_time => undef,
2345 fulfillment_time => undef
2349 flesh_hold_transits($holds);
2354 sub flesh_hold_transits {
2356 for my $hold ( @$holds ) {
2358 $apputils->simplereq(
2360 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
2361 { hold => $hold->id, cancel_time => undef },
2362 { order_by => { ahtc => 'id desc' }, limit => 1 }
2368 sub flesh_hold_notices {
2369 my( $holds, $e ) = @_;
2370 $e ||= new_editor();
2372 for my $hold (@$holds) {
2373 my $notices = $e->search_action_hold_notification(
2375 { hold => $hold->id },
2376 { order_by => { anh => 'notify_time desc' } },
2381 $hold->notify_count(scalar(@$notices));
2383 my $n = $e->retrieve_action_hold_notification($$notices[0])
2384 or return $e->event;
2385 $hold->notify_time($n->notify_time);
2391 __PACKAGE__->register_method(
2392 method => 'fetch_captured_holds',
2393 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
2397 Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
2398 @param authtoken The login session key
2399 @param org The org id of the location in question
2400 @param match_copy A specific copy to limit to
2404 __PACKAGE__->register_method(
2405 method => 'fetch_captured_holds',
2406 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
2410 Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
2411 @param authtoken The login session key
2412 @param org The org id of the location in question
2413 @param match_copy A specific copy to limit to
2417 __PACKAGE__->register_method(
2418 method => 'fetch_captured_holds',
2419 api_name => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
2423 Returns list ids of shelf-expired un-fulfilled holds for a given title id
2424 @param authtoken The login session key
2425 @param org The org id of the location in question
2426 @param match_copy A specific copy to limit to
2430 __PACKAGE__->register_method(
2431 method => 'fetch_captured_holds',
2433 'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
2437 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2438 for a given shelf lib
2442 __PACKAGE__->register_method(
2443 method => 'fetch_captured_holds',
2445 'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
2449 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2450 for a given shelf lib
2455 sub fetch_captured_holds {
2456 my( $self, $conn, $auth, $org, $match_copy ) = @_;
2458 my $e = new_editor(authtoken => $auth);
2459 return $e->die_event unless $e->checkauth;
2460 return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
2462 $org ||= $e->requestor->ws_ou;
2464 my $current_copy = { '!=' => undef };
2465 $current_copy = { '=' => $match_copy } if $match_copy;
2468 select => { alhr => ['id'] },
2473 fkey => 'current_copy'
2478 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
2480 capture_time => { "!=" => undef },
2481 current_copy => $current_copy,
2482 fulfillment_time => undef,
2483 current_shelf_lib => $org
2487 if($self->api_name =~ /expired/) {
2488 $query->{'where'}->{'+alhr'}->{'-or'} = {
2489 shelf_expire_time => { '<' => 'today'},
2490 cancel_time => { '!=' => undef },
2493 my $hold_ids = $e->json_query( $query );
2495 if ($self->api_name =~ /wrong_shelf/) {
2496 # fetch holds whose current_shelf_lib is $org, but whose pickup
2497 # lib is some other org unit. Ignore already-retrieved holds.
2499 pickup_lib_changed_on_shelf_holds(
2500 $e, $org, [map {$_->{id}} @$hold_ids]);
2501 # match the layout of other items in $hold_ids
2502 push (@$hold_ids, {id => $_}) for @$wrong_shelf;
2506 for my $hold_id (@$hold_ids) {
2507 if($self->api_name =~ /id_list/) {
2508 $conn->respond($hold_id->{id});
2512 $e->retrieve_action_hold_request([
2516 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
2517 order_by => {anh => 'notify_time desc'}
2527 __PACKAGE__->register_method(
2528 method => "print_expired_holds_stream",
2529 api_name => "open-ils.circ.captured_holds.expired.print.stream",
2533 sub print_expired_holds_stream {
2534 my ($self, $client, $auth, $params) = @_;
2536 # No need to check specific permissions: we're going to call another method
2537 # that will do that.
2538 my $e = new_editor("authtoken" => $auth);
2539 return $e->die_event unless $e->checkauth;
2541 delete($$params{org_id}) unless (int($$params{org_id}));
2542 delete($$params{limit}) unless (int($$params{limit}));
2543 delete($$params{offset}) unless (int($$params{offset}));
2544 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2545 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2546 $$params{chunk_size} ||= 10;
2547 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2549 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2551 my @hold_ids = $self->method_lookup(
2552 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
2553 )->run($auth, $params->{"org_id"});
2558 } elsif (defined $U->event_code($hold_ids[0])) {
2560 return $hold_ids[0];
2563 $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
2566 my @hid_chunk = splice @hold_ids, 0, $params->{"chunk_size"};
2568 my $result_chunk = $e->json_query({
2570 "acp" => ["barcode"],
2572 first_given_name second_given_name family_name alias
2581 "field" => "id", "fkey" => "current_copy",
2584 "field" => "id", "fkey" => "call_number",
2587 "field" => "id", "fkey" => "record"
2591 "acpl" => {"field" => "id", "fkey" => "location"}
2594 "au" => {"field" => "id", "fkey" => "usr"}
2597 "where" => {"+ahr" => {"id" => \@hid_chunk}}
2598 }) or return $e->die_event;
2599 $client->respond($result_chunk);
2606 __PACKAGE__->register_method(
2607 method => "check_title_hold_batch",
2608 api_name => "open-ils.circ.title_hold.is_possible.batch",
2611 desc => '@see open-ils.circ.title_hold.is_possible.batch',
2613 { desc => 'Authentication token', type => 'string'},
2614 { desc => 'Array of Hash of named parameters', type => 'array'},
2617 desc => 'Array of response objects',
2623 sub check_title_hold_batch {
2624 my($self, $client, $authtoken, $param_list, $oargs) = @_;
2625 foreach (@$param_list) {
2626 my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
2627 $client->respond($res);
2633 __PACKAGE__->register_method(
2634 method => "check_title_hold",
2635 api_name => "open-ils.circ.title_hold.is_possible",
2637 desc => 'Determines if a hold were to be placed by a given user, ' .
2638 'whether or not said hold would have any potential copies to fulfill it.' .
2639 'The named paramaters of the second argument include: ' .
2640 'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
2641 'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
2643 { desc => 'Authentication token', type => 'string'},
2644 { desc => 'Hash of named parameters', type => 'object'},
2647 desc => 'List of new message IDs (empty if none)',
2653 =head3 check_title_hold (token, hash)
2655 The named fields in the hash are:
2657 patronid - ID of the hold recipient (required)
2658 depth - hold range depth (default 0)
2659 pickup_lib - destination for hold, fallback value for selection_ou
2660 selection_ou - ID of org_unit establishing hard and soft hold boundary settings
2661 issuanceid - ID of the issuance to be held, required for Issuance level hold
2662 partid - ID of the monograph part to be held, required for monograph part level hold
2663 titleid - ID (BRN) of the title to be held, required for Title level hold
2664 volume_id - required for Volume level hold
2665 copy_id - required for Copy level hold
2666 mrid - required for Meta-record level hold
2667 hold_type - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record (default "T")
2669 All key/value pairs are passed on to do_possibility_checks.
2673 # FIXME: better params checking. what other params are required, if any?
2674 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
2675 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
2676 # used in conditionals, where it may be undefined, causing a warning.
2677 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
2679 sub check_title_hold {
2680 my( $self, $client, $authtoken, $params ) = @_;
2681 my $e = new_editor(authtoken=>$authtoken);
2682 return $e->event unless $e->checkauth;
2684 my %params = %$params;
2685 my $depth = $params{depth} || 0;
2686 $params{depth} = $depth; #define $params{depth} if unset, since it gets used later
2687 my $selection_ou = $params{selection_ou} || $params{pickup_lib};
2688 my $oargs = $params{oargs} || {};
2690 if($oargs->{events}) {
2691 @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
2695 my $patron = $e->retrieve_actor_user($params{patronid})
2696 or return $e->event;
2698 if( $e->requestor->id ne $patron->id ) {
2699 return $e->event unless
2700 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
2703 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
2705 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
2706 or return $e->event;
2708 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
2709 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
2712 my $return_depth = $hard_boundary; # default depth to return on success
2713 if(defined $soft_boundary and $depth < $soft_boundary) {
2714 # work up the tree and as soon as we find a potential copy, use that depth
2715 # also, make sure we don't go past the hard boundary if it exists
2717 # our min boundary is the greater of user-specified boundary or hard boundary
2718 my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
2719 $hard_boundary : $depth;
2721 my $depth = $soft_boundary;
2722 while($depth >= $min_depth) {
2723 $logger->info("performing hold possibility check with soft boundary $depth");
2724 @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
2726 $return_depth = $depth;
2731 } elsif(defined $hard_boundary and $depth < $hard_boundary) {
2732 # there is no soft boundary, enforce the hard boundary if it exists
2733 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
2734 @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
2736 # no boundaries defined, fall back to user specifed boundary or no boundary
2737 $logger->info("performing hold possibility check with no boundary");
2738 @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
2741 my $place_unfillable = 0;
2742 $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
2747 "depth" => $return_depth,
2748 "local_avail" => $status[1]
2750 } elsif ($status[2]) {
2751 my $n = scalar @{$status[2]};
2752 return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2754 return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2760 sub do_possibility_checks {
2761 my($e, $patron, $request_lib, $depth, %params) = @_;
2763 my $issuanceid = $params{issuanceid} || "";
2764 my $partid = $params{partid} || "";
2765 my $titleid = $params{titleid} || "";
2766 my $volid = $params{volume_id};
2767 my $copyid = $params{copy_id};
2768 my $mrid = $params{mrid} || "";
2769 my $pickup_lib = $params{pickup_lib};
2770 my $hold_type = $params{hold_type} || 'T';
2771 my $selection_ou = $params{selection_ou} || $pickup_lib;
2772 my $holdable_formats = $params{holdable_formats};
2773 my $oargs = $params{oargs} || {};
2780 if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
2782 return $e->event unless $copy = $e->retrieve_asset_copy($copyid);
2783 return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
2784 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2786 return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
2787 return verify_copy_for_hold(
2788 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
2791 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2793 return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
2794 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2796 return _check_volume_hold_is_possible(
2797 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2800 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
2802 return _check_title_hold_is_possible(
2803 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
2806 } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
2808 return _check_issuance_hold_is_possible(
2809 $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2812 } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
2814 return _check_monopart_hold_is_possible(
2815 $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2818 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2820 # pasing undef as the depth to filtered_records causes the depth
2821 # of the selection_ou to be used, which is not what we want here.
2824 my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
2826 for my $rec (@$recs) {
2827 @status = _check_title_hold_is_possible(
2828 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
2834 # else { Unrecognized hold_type ! } # FIXME: return error? or 0?
2837 sub MR_filter_records {
2844 my $opac_visible = shift;
2846 my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
2847 return $U->storagereq(
2848 'open-ils.storage.metarecord.filtered_records.atomic',
2849 $m, $f, $org_at_depth, $opac_visible
2852 __PACKAGE__->register_method(
2853 method => 'MR_filter_records',
2854 api_name => 'open-ils.circ.holds.metarecord.filtered_records',
2859 sub create_ranged_org_filter {
2860 my($e, $selection_ou, $depth) = @_;
2862 # find the orgs from which this hold may be fulfilled,
2863 # based on the selection_ou and depth
2865 my $top_org = $e->search_actor_org_unit([
2866 {parent_ou => undef},
2867 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
2870 return () if $depth == $top_org->ou_type->depth;
2872 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
2873 %org_filter = (circ_lib => []);
2874 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
2876 $logger->info("hold org filter at depth $depth and selection_ou ".
2877 "$selection_ou created list of @{$org_filter{circ_lib}}");
2883 sub _check_title_hold_is_possible {
2884 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
2885 # $holdable_formats is now unused. We pre-filter the MR's records.
2887 my $e = new_editor();
2889 # T holds on records that have parts are normally OK, but if the record has
2890 # no non-part copies, the hold will ultimately fail, so let's test for that.
2892 # If the global flag circ.holds.api_require_monographic_part_when_present is
2893 # enabled, then any configured parts for the bib is enough to disallow title holds.
2894 my $part_required = 0;
2895 my $parts = $e->search_biblio_monograph_part(
2902 my $part_required_flag = $e->retrieve_config_global_flag('circ.holds.api_require_monographic_part_when_present');
2903 $part_required = ($part_required_flag and $U->is_true($part_required_flag->enabled));
2904 if (!$part_required) {
2905 my $np_copies = $e->json_query({
2906 select => { acp => [{column => 'id', transform => 'count', alias => 'count'}]},
2907 from => {acp => {acn => {}, acpm => {type => 'left'}}},
2909 '+acp' => {deleted => 'f'},
2910 '+acn' => {deleted => 'f', record => $titleid},
2911 '+acpm' => {id => undef}
2914 $part_required = 1 if $np_copies->[0]->{count} == 0;
2917 if ($part_required) {
2918 $logger->info("title hold when monographic part required");
2922 "TITLE_HOLD_WHEN_MONOGRAPHIC_PART_REQUIRED",
2923 "payload" => {"fail_part" => "monographic_part_required"}
2929 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2931 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2932 my $copies = $e->json_query(
2934 select => { acp => ['id', 'circ_lib'] },
2939 fkey => 'call_number',
2940 filter => { record => $titleid }
2944 filter => { holdable => 't', deleted => 'f' },
2947 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' },
2948 acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2952 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2953 '+acpm' => { target_copy => undef } # ignore part-linked copies
2958 $logger->info("title possible found ".scalar(@$copies)." potential copies");
2962 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2963 "payload" => {"fail_part" => "no_ultimate_items"}
2968 # -----------------------------------------------------------------------
2969 # sort the copies into buckets based on their circ_lib proximity to
2970 # the patron's home_ou.
2971 # -----------------------------------------------------------------------
2973 my $home_org = $patron->home_ou;
2974 my $req_org = $request_lib->id;
2976 $prox_cache{$home_org} =
2977 $e->search_actor_org_unit_proximity({from_org => $home_org})
2978 unless $prox_cache{$home_org};
2979 my $home_prox = $prox_cache{$home_org};
2980 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2983 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2984 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2986 my @keys = sort { $a <=> $b } keys %buckets;
2989 if( $home_org ne $req_org ) {
2990 # -----------------------------------------------------------------------
2991 # shove the copies close to the request_lib into the primary buckets
2992 # directly before the farthest away copies. That way, they are not
2993 # given priority, but they are checked before the farthest copies.
2994 # -----------------------------------------------------------------------
2995 $prox_cache{$req_org} =
2996 $e->search_actor_org_unit_proximity({from_org => $req_org})
2997 unless $prox_cache{$req_org};
2998 my $req_prox = $prox_cache{$req_org};
3001 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3002 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3004 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
3005 my $new_key = $highest_key - 0.5; # right before the farthest prox
3006 my @keys2 = sort { $a <=> $b } keys %buckets2;
3007 for my $key (@keys2) {
3008 last if $key >= $highest_key;
3009 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3013 @keys = sort { $a <=> $b } keys %buckets;
3018 my $age_protect_only = 0;
3019 OUTER: for my $key (@keys) {
3020 my @cps = @{$buckets{$key}};
3022 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3024 for my $copyid (@cps) {
3026 next if $seen{$copyid};
3027 $seen{$copyid} = 1; # there could be dupes given the merged buckets
3028 my $copy = $e->retrieve_asset_copy($copyid);
3029 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3031 unless($title) { # grab the title if we don't already have it
3032 my $vol = $e->retrieve_asset_call_number(
3033 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3034 $title = $vol->record;
3037 @status = verify_copy_for_hold(
3038 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3040 $age_protect_only ||= $status[3];
3041 last OUTER if $status[0];
3045 $status[3] = $age_protect_only;
3049 sub _check_issuance_hold_is_possible {
3050 my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3052 my $e = new_editor();
3053 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
3055 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
3056 my $copies = $e->json_query(
3058 select => { acp => ['id', 'circ_lib'] },
3064 filter => { issuance => $issuanceid }
3068 filter => { holdable => 't', deleted => 'f' },
3071 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3075 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3081 $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
3085 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3086 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3091 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3092 "payload" => {"fail_part" => "no_ultimate_items"}
3100 # -----------------------------------------------------------------------
3101 # sort the copies into buckets based on their circ_lib proximity to
3102 # the patron's home_ou.
3103 # -----------------------------------------------------------------------
3105 my $home_org = $patron->home_ou;
3106 my $req_org = $request_lib->id;
3108 $prox_cache{$home_org} =
3109 $e->search_actor_org_unit_proximity({from_org => $home_org})
3110 unless $prox_cache{$home_org};
3111 my $home_prox = $prox_cache{$home_org};
3112 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3115 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3116 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3118 my @keys = sort { $a <=> $b } keys %buckets;
3121 if( $home_org ne $req_org ) {
3122 # -----------------------------------------------------------------------
3123 # shove the copies close to the request_lib into the primary buckets
3124 # directly before the farthest away copies. That way, they are not
3125 # given priority, but they are checked before the farthest copies.
3126 # -----------------------------------------------------------------------
3127 $prox_cache{$req_org} =
3128 $e->search_actor_org_unit_proximity({from_org => $req_org})
3129 unless $prox_cache{$req_org};
3130 my $req_prox = $prox_cache{$req_org};
3133 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3134 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3136 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
3137 my $new_key = $highest_key - 0.5; # right before the farthest prox
3138 my @keys2 = sort { $a <=> $b } keys %buckets2;
3139 for my $key (@keys2) {
3140 last if $key >= $highest_key;
3141 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3145 @keys = sort { $a <=> $b } keys %buckets;
3150 my $age_protect_only = 0;
3151 OUTER: for my $key (@keys) {
3152 my @cps = @{$buckets{$key}};
3154 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3156 for my $copyid (@cps) {
3158 next if $seen{$copyid};
3159 $seen{$copyid} = 1; # there could be dupes given the merged buckets
3160 my $copy = $e->retrieve_asset_copy($copyid);
3161 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3163 unless($title) { # grab the title if we don't already have it
3164 my $vol = $e->retrieve_asset_call_number(
3165 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3166 $title = $vol->record;
3169 @status = verify_copy_for_hold(
3170 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3172 $age_protect_only ||= $status[3];
3173 last OUTER if $status[0];
3178 if (!defined($empty_ok)) {
3179 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
3180 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3183 return (1,0) if ($empty_ok);
3185 $status[3] = $age_protect_only;
3189 sub _check_monopart_hold_is_possible {
3190 my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3192 my $e = new_editor();
3193 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
3195 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
3196 my $copies = $e->json_query(
3198 select => { acp => ['id', 'circ_lib'] },
3202 field => 'target_copy',
3204 filter => { part => $partid }
3208 filter => { holdable => 't', deleted => 'f' },
3211 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3215 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
3221 $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
3225 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3226 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3231 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3232 "payload" => {"fail_part" => "no_ultimate_items"}
3240 # -----------------------------------------------------------------------
3241 # sort the copies into buckets based on their circ_lib proximity to
3242 # the patron's home_ou.
3243 # -----------------------------------------------------------------------
3245 my $home_org = $patron->home_ou;
3246 my $req_org = $request_lib->id;
3248 $prox_cache{$home_org} =
3249 $e->search_actor_org_unit_proximity({from_org => $home_org})
3250 unless $prox_cache{$home_org};
3251 my $home_prox = $prox_cache{$home_org};
3252 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
3255 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
3256 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3258 my @keys = sort { $a <=> $b } keys %buckets;
3261 if( $home_org ne $req_org ) {
3262 # -----------------------------------------------------------------------
3263 # shove the copies close to the request_lib into the primary buckets
3264 # directly before the farthest away copies. That way, they are not
3265 # given priority, but they are checked before the farthest copies.
3266 # -----------------------------------------------------------------------
3267 $prox_cache{$req_org} =
3268 $e->search_actor_org_unit_proximity({from_org => $req_org})
3269 unless $prox_cache{$req_org};
3270 my $req_prox = $prox_cache{$req_org};
3273 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
3274 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
3276 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
3277 my $new_key = $highest_key - 0.5; # right before the farthest prox
3278 my @keys2 = sort { $a <=> $b } keys %buckets2;
3279 for my $key (@keys2) {
3280 last if $key >= $highest_key;
3281 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
3285 @keys = sort { $a <=> $b } keys %buckets;
3290 my $age_protect_only = 0;
3291 OUTER: for my $key (@keys) {
3292 my @cps = @{$buckets{$key}};
3294 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
3296 for my $copyid (@cps) {
3298 next if $seen{$copyid};
3299 $seen{$copyid} = 1; # there could be dupes given the merged buckets
3300 my $copy = $e->retrieve_asset_copy($copyid);
3301 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
3303 unless($title) { # grab the title if we don't already have it
3304 my $vol = $e->retrieve_asset_call_number(
3305 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
3306 $title = $vol->record;
3309 @status = verify_copy_for_hold(
3310 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
3312 $age_protect_only ||= $status[3];
3313 last OUTER if $status[0];
3318 if (!defined($empty_ok)) {
3319 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3320 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3323 return (1,0) if ($empty_ok);
3325 $status[3] = $age_protect_only;
3330 sub _check_volume_hold_is_possible {
3331 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3332 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
3333 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
3334 $logger->info("checking possibility of volume hold for volume ".$vol->id);
3336 my $filter_copies = [];
3337 for my $copy (@$copies) {
3338 # ignore part-mapped copies for regular volume level holds
3339 push(@$filter_copies, $copy) unless
3340 new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
3342 $copies = $filter_copies;
3347 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3348 "payload" => {"fail_part" => "no_ultimate_items"}
3354 my $age_protect_only = 0;
3355 for my $copy ( @$copies ) {
3356 @status = verify_copy_for_hold(
3357 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
3358 $age_protect_only ||= $status[3];
3361 $status[3] = $age_protect_only;
3367 sub verify_copy_for_hold {
3368 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
3369 # $oargs should be undef unless we're overriding.
3370 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
3371 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3374 requestor => $requestor,
3377 title_descriptor => $title->fixed_fields,
3378 pickup_lib => $pickup_lib,
3379 request_lib => $request_lib,
3381 show_event_list => 1
3385 # Check for override permissions on events.
3386 if ($oargs && $permitted && scalar @$permitted) {
3387 # Remove the events from permitted that we can override.
3388 if ($oargs->{events}) {
3389 foreach my $evt (@{$oargs->{events}}) {
3390 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3393 # Now, we handle the override all case by checking remaining
3394 # events against override permissions.
3395 if (scalar @$permitted && $oargs->{all}) {
3396 # Pre-set events and failed members of oargs to empty
3397 # arrays, if they are not set, yet.
3398 $oargs->{events} = [] unless ($oargs->{events});
3399 $oargs->{failed} = [] unless ($oargs->{failed});
3400 # When we're done with these checks, we swap permitted
3401 # with a reference to @disallowed.
3402 my @disallowed = ();
3403 foreach my $evt (@{$permitted}) {
3404 # Check if we've already seen the event in this
3405 # session and it failed.
3406 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3407 push(@disallowed, $evt);
3409 # We have to check if the requestor has the
3410 # override permission.
3412 # AppUtils::check_user_perms returns the perm if
3413 # the user doesn't have it, undef if they do.
3414 if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3415 push(@disallowed, $evt);
3416 push(@{$oargs->{failed}}, $evt->{textcode});
3418 push(@{$oargs->{events}}, $evt->{textcode});
3422 $permitted = \@disallowed;
3426 my $age_protect_only = 0;
3427 if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3428 $age_protect_only = 1;
3432 (not scalar @$permitted), # true if permitted is an empty arrayref
3433 ( # XXX This test is of very dubious value; someone should figure
3434 # out what if anything is checking this value
3435 ($copy->circ_lib == $pickup_lib) and
3436 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3445 sub find_nearest_permitted_hold {
3448 my $editor = shift; # CStoreEditor object
3449 my $copy = shift; # copy to target
3450 my $user = shift; # staff
3451 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3453 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3455 my $bc = $copy->barcode;
3457 # find any existing holds that already target this copy
3458 my $old_holds = $editor->search_action_hold_request(
3459 { current_copy => $copy->id,
3460 cancel_time => undef,
3461 capture_time => undef
3465 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3467 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3468 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3470 my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3472 # the nearest_hold API call now needs this
3473 $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3474 unless ref $copy->call_number;
3476 # search for what should be the best holds for this copy to fulfill
3477 my $best_holds = $U->storagereq(
3478 "open-ils.storage.action.hold_request.nearest_hold.atomic",
3479 $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3481 # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3483 for my $holdid (@$old_holds) {
3484 next unless $holdid;
3485 push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3489 unless(@$best_holds) {
3490 $logger->info("circulator: no suitable holds found for copy $bc");
3491 return (undef, $evt);
3497 # for each potential hold, we have to run the permit script
3498 # to make sure the hold is actually permitted.
3501 for my $holdid (@$best_holds) {
3502 next unless $holdid;
3503 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3505 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3506 # Force and recall holds bypass all rules
3507 if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3511 my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3512 my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3514 $reqr_cache{$hold->requestor} = $reqr;
3515 $org_cache{$hold->request_lib} = $rlib;
3517 # see if this hold is permitted
3518 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3520 patron_id => $hold->usr,
3523 pickup_lib => $hold->pickup_lib,
3524 request_lib => $rlib,
3536 unless( $best_hold ) { # no "good" permitted holds were found
3538 $logger->info("circulator: no suitable holds found for copy $bc");
3539 return (undef, $evt);
3542 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3544 # indicate a permitted hold was found
3545 return $best_hold if $check_only;
3547 # we've found a permitted hold. we need to "grab" the copy
3548 # to prevent re-targeted holds (next part) from re-grabbing the copy
3549 $best_hold->current_copy($copy->id);
3550 $editor->update_action_hold_request($best_hold)
3551 or return (undef, $editor->event);
3556 # re-target any other holds that already target this copy
3557 for my $old_hold (@$old_holds) {
3558 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3559 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3560 $old_hold->id." after a better hold [".$best_hold->id."] was found");
3561 $old_hold->clear_current_copy;
3562 $old_hold->clear_prev_check_time;
3563 $editor->update_action_hold_request($old_hold)
3564 or return (undef, $editor->event);
3565 push(@retarget, $old_hold->id);
3568 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3576 __PACKAGE__->register_method(
3577 method => 'all_rec_holds',
3578 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3582 my( $self, $conn, $auth, $title_id, $args ) = @_;
3584 my $e = new_editor(authtoken=>$auth);
3585 $e->checkauth or return $e->event;
3586 $e->allowed('VIEW_HOLD') or return $e->event;
3589 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
3590 $args->{cancel_time} = undef;
3593 metarecord_holds => []
3595 , volume_holds => []
3597 , recall_holds => []
3600 , issuance_holds => []
3603 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3605 $resp->{metarecord_holds} = $e->search_action_hold_request(
3606 { hold_type => OILS_HOLD_TYPE_METARECORD,
3607 target => $mr_map->metarecord,
3613 $resp->{title_holds} = $e->search_action_hold_request(
3615 hold_type => OILS_HOLD_TYPE_TITLE,
3616 target => $title_id,
3620 my $parts = $e->search_biblio_monograph_part(
3626 $resp->{part_holds} = $e->search_action_hold_request(
3628 hold_type => OILS_HOLD_TYPE_MONOPART,
3634 my $subs = $e->search_serial_subscription(
3635 { record_entry => $title_id }, {idlist=>1});
3638 my $issuances = $e->search_serial_issuance(
3639 {subscription => $subs}, {idlist=>1}
3643 $resp->{issuance_holds} = $e->search_action_hold_request(
3645 hold_type => OILS_HOLD_TYPE_ISSUANCE,
3646 target => $issuances,
3653 my $vols = $e->search_asset_call_number(
3654 { record => $title_id, deleted => 'f' }, {idlist=>1});
3656 return $resp unless @$vols;
3658 $resp->{volume_holds} = $e->search_action_hold_request(
3660 hold_type => OILS_HOLD_TYPE_VOLUME,
3665 my $copies = $e->search_asset_copy(
3666 { call_number => $vols, deleted => 'f' }, {idlist=>1});
3668 return $resp unless @$copies;
3670 $resp->{copy_holds} = $e->search_action_hold_request(
3672 hold_type => OILS_HOLD_TYPE_COPY,
3677 $resp->{recall_holds} = $e->search_action_hold_request(
3679 hold_type => OILS_HOLD_TYPE_RECALL,
3684 $resp->{force_holds} = $e->search_action_hold_request(
3686 hold_type => OILS_HOLD_TYPE_FORCE,
3694 __PACKAGE__->register_method(
3695 method => 'stream_wide_holds',
3698 api_name => 'open-ils.circ.hold.wide_hash.stream'
3701 sub stream_wide_holds {
3702 my($self, $client, $auth, $restrictions, $order_by, $limit, $offset, $options) = @_;
3705 my $e = new_editor(authtoken=>$auth);
3706 $e->checkauth or return $e->event;
3707 $e->allowed('VIEW_HOLD') or return $e->event;
3709 if ($options->{recently_canceled}) {
3710 # Map the the recently canceled holds filter into values
3711 # wide-stream understands.
3712 my $filter = recently_canceled_holds_filter($e);
3713 $restrictions->{$_} =
3714 $filter->{where}->{$_} for keys %{$filter->{where}};
3716 $limit = $filter->{limit} if $filter->{limit};
3719 my $filters = OpenSRF::Utils::JSON->perl2JSON($restrictions);
3720 $logger->info("WIDE HOLD FILTERS: $filters");
3722 my $st = OpenSRF::AppSession->create('open-ils.storage');
3723 my $req = $st->request(
3724 'open-ils.storage.action.live_holds.wide_hash.atomic',
3725 $restrictions, $order_by, $limit, $offset
3728 my $results = $req->recv;
3733 if(UNIVERSAL::isa($results,"Error")) {
3734 throw OpenSRF::EX::ERROR ("Error fetch hold shelf list");
3737 my @rows = @{ $results->content };
3739 # Force immediate send of count response
3740 my $mbc = $client->max_bundle_count;
3741 $client->max_bundle_count(1);
3742 $client->respond(shift @rows);
3743 $client->max_bundle_count($mbc);
3745 foreach my $hold (@rows) {
3746 $client->respond($hold) if $hold;
3749 $client->respond_complete;
3755 __PACKAGE__->register_method(
3756 method => 'uber_hold',
3758 api_name => 'open-ils.circ.hold.details.retrieve'
3762 my($self, $client, $auth, $hold_id, $args) = @_;
3763 my $e = new_editor(authtoken=>$auth);
3764 $e->checkauth or return $e->event;
3765 return uber_hold_impl($e, $hold_id, $args);
3768 __PACKAGE__->register_method(
3769 method => 'batch_uber_hold',
3772 api_name => 'open-ils.circ.hold.details.batch.retrieve'
3775 sub batch_uber_hold {
3776 my($self, $client, $auth, $hold_ids, $args) = @_;
3777 my $e = new_editor(authtoken=>$auth);
3778 $e->checkauth or return $e->event;
3779 $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3783 sub uber_hold_impl {
3784 my($e, $hold_id, $args) = @_;
3787 my $flesh_fields = ['current_copy', 'usr', 'notes'];
3788 push (@$flesh_fields, 'requestor') if $args->{include_requestor};
3789 push (@$flesh_fields, 'cancel_cause') if $args->{include_cancel_cause};
3790 push (@$flesh_fields, 'sms_carrier') if $args->{include_sms_carrier};
3792 my $hold = $e->retrieve_action_hold_request([
3794 {flesh => 1, flesh_fields => {ahr => $flesh_fields}}
3795 ]) or return $e->event;
3797 if($hold->usr->id ne $e->requestor->id) {
3798 # caller is asking for someone else's hold
3799 $e->allowed('VIEW_HOLD') or return $e->event;
3800 $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3801 [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3804 # caller is asking for own hold, but may not have permission to view staff notes
3805 unless($e->allowed('VIEW_HOLD')) {
3806 $hold->notes( # filter out any staff notes (unless marked as public)
3807 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3811 my $user = $hold->usr;
3812 $hold->usr($user->id);
3815 my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args);
3817 flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3818 flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3820 my $details = retrieve_hold_queue_status_impl($e, $hold);
3821 $hold->usr($user) if $args->{include_usr}; # re-flesh
3826 ($copy ? (copy => $copy) : ()),
3827 ($volume ? (volume => $volume) : ()),
3828 ($issuance ? (issuance => $issuance) : ()),
3829 ($part ? (part => $part) : ()),
3830 ($args->{include_bre} ? (bre => $bre) : ()),
3831 ($args->{suppress_mvr} ? () : (mvr => $mvr)),
3835 $resp->{copy}->location(
3836 $e->retrieve_asset_copy_location($resp->{copy}->location))
3837 if $resp->{copy} and $args->{flesh_acpl};
3839 unless($args->{suppress_patron_details}) {
3840 my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3841 $resp->{patron_first} = $user->first_given_name,
3842 $resp->{patron_last} = $user->family_name,
3843 $resp->{patron_barcode} = $card->barcode,
3844 $resp->{patron_alias} = $user->alias,
3852 # -----------------------------------------------------
3853 # Returns the MVR object that represents what the
3855 # -----------------------------------------------------
3857 my( $e, $hold, $args ) = @_;
3865 my $no_mvr = $args->{suppress_mvr};
3867 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3868 $metarecord = $e->retrieve_metabib_metarecord($hold->target)
3869 or return $e->event;
3870 $tid = $metarecord->master_record;
3872 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3873 $tid = $hold->target;
3875 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3876 $volume = $e->retrieve_asset_call_number($hold->target)
3877 or return $e->event;
3878 $tid = $volume->record;
3880 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3881 $issuance = $e->retrieve_serial_issuance([
3883 {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3884 ]) or return $e->event;
3886 $tid = $issuance->subscription->record_entry;
3888 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3889 $part = $e->retrieve_biblio_monograph_part([
3891 ]) or return $e->event;
3893 $tid = $part->record;
3895 } 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 ) {
3896 $copy = $e->retrieve_asset_copy([
3898 {flesh => 1, flesh_fields => {acp => ['call_number']}}
3899 ]) or return $e->event;
3901 $volume = $copy->call_number;
3902 $tid = $volume->record;
3905 if(!$copy and ref $hold->current_copy ) {
3906 $copy = $hold->current_copy;
3907 $hold->current_copy($copy->id) unless $args->{include_current_copy};
3910 if(!$volume and $copy) {
3911 $volume = $e->retrieve_asset_call_number($copy->call_number);
3914 # TODO return metarcord mvr for M holds
3915 my $title = $e->retrieve_biblio_record_entry($tid);
3916 return ( ($no_mvr) ? undef : $U->record_to_mvr($title),
3917 $volume, $copy, $issuance, $part, $title, $metarecord);
3920 __PACKAGE__->register_method(
3921 method => 'clear_shelf_cache',
3922 api_name => 'open-ils.circ.hold.clear_shelf.get_cache',
3926 Returns the holds processed with the given cache key
3931 sub clear_shelf_cache {
3932 my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3933 my $e = new_editor(authtoken => $auth, xact => 1);
3934 return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3937 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3939 my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3942 $logger->info("no hold data found in cache"); # XXX TODO return event
3948 foreach (keys %$hold_data) {
3949 $maximum += scalar(@{ $hold_data->{$_} });
3951 $client->respond({"maximum" => $maximum, "progress" => 0});
3953 for my $action (sort keys %$hold_data) {
3954 while (@{$hold_data->{$action}}) {
3955 my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3957 my $result_chunk = $e->json_query({
3959 "acp" => ["barcode"],
3961 first_given_name second_given_name family_name alias
3964 "acnp" => [{column => "label", alias => "prefix"}],
3965 "acns" => [{column => "label", alias => "suffix"}],
3973 "field" => "id", "fkey" => "current_copy",
3976 "field" => "id", "fkey" => "call_number",
3979 "field" => "id", "fkey" => "record"
3982 "field" => "id", "fkey" => "prefix"
3985 "field" => "id", "fkey" => "suffix"
3989 "acpl" => {"field" => "id", "fkey" => "location"}
3992 "au" => {"field" => "id", "fkey" => "usr"}
3995 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3996 }, {"substream" => 1}) or return $e->die_event;
4000 +{"action" => $action, "hold_details" => $_}
4011 __PACKAGE__->register_method(
4012 method => 'clear_shelf_process',
4014 api_name => 'open-ils.circ.hold.clear_shelf.process',
4017 1. Find all holds that have expired on the holds shelf
4019 3. If a clear-shelf status is configured, put targeted copies into this status
4020 4. Divide copies into 3 groups: items to transit, items to reshelve, and items
4021 that are needed for holds. No subsequent action is taken on the holds
4022 or items after grouping.
4027 sub clear_shelf_process {
4028 my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
4030 my $e = new_editor(authtoken=>$auth);
4031 $e->checkauth or return $e->die_event;
4032 my $cache = OpenSRF::Utils::Cache->new('global');
4034 $org_id ||= $e->requestor->ws_ou;
4035 $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
4037 my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
4039 my @hold_ids = $self->method_lookup(
4040 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
4041 )->run($auth, $org_id, $match_copy);
4046 my @canceled_holds; # newly canceled holds
4047 $chunk_size ||= 25; # chunked status updates
4048 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
4051 for my $hold_id (@hold_ids) {
4053 $logger->info("Clear shelf processing hold $hold_id");
4055 my $hold = $e->retrieve_action_hold_request([
4058 flesh_fields => {ahr => ['current_copy']}
4062 if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
4063 $hold->cancel_time('now');
4064 $hold->cancel_cause(2); # Hold Shelf expiration
4065 $e->update_action_hold_request($hold) or return $e->die_event;
4066 push(@canceled_holds, $hold_id);
4069 my $copy = $hold->current_copy;
4071 if($copy_status or $copy_status == 0) {
4072 # if a clear-shelf copy status is defined, update the copy
4073 $copy->status($copy_status);
4074 $copy->edit_date('now');
4075 $copy->editor($e->requestor->id);
4076 $e->update_asset_copy($copy) or return $e->die_event;
4079 push(@holds, $hold);
4080 $client->respond({maximum => int(scalar(@holds)), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
4089 pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
4092 for my $hold (@holds) {
4094 my $copy = $hold->current_copy;
4095 my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
4097 if($alt_hold and !$match_copy) {
4099 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
4101 } elsif($copy->circ_lib != $e->requestor->ws_ou) {
4103 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
4107 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
4111 my $cache_key = md5_hex(time . $$ . rand());
4112 $logger->info("clear_shelf_cache: storing under $cache_key");
4113 $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours. configurable?
4115 # tell the client we're done
4116 $client->respond_complete({cache_key => $cache_key});
4119 # fire off the hold cancelation trigger and wait for response so don't flood the service
4121 # refetch the holds to pick up the caclulated cancel_time,
4122 # which may be needed by Action/Trigger
4124 my $updated_holds = [];
4125 $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
4128 $U->create_events_for_hook(
4129 'hold_request.cancel.expire_holds_shelf',
4130 $_, $org_id, undef, undef, 1) for @$updated_holds;
4133 # tell the client we're done
4134 $client->respond_complete;
4138 # returns IDs for holds that are on the holds shelf but
4139 # have had their pickup_libs change while on the shelf.
4140 sub pickup_lib_changed_on_shelf_holds {
4143 my $ignore_holds = shift;
4144 $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
4147 select => { alhr => ['id'] },
4152 fkey => 'current_copy'
4157 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
4159 capture_time => { "!=" => undef },
4160 fulfillment_time => undef,
4161 current_shelf_lib => $org_id,
4162 pickup_lib => {'!=' => {'+alhr' => 'current_shelf_lib'}}
4167 $query->{where}->{'+alhr'}->{id} =
4168 {'not in' => $ignore_holds} if @$ignore_holds;
4170 my $hold_ids = $e->json_query($query);
4171 return [ map { $_->{id} } @$hold_ids ];
4174 __PACKAGE__->register_method(
4175 method => 'usr_hold_summary',
4176 api_name => 'open-ils.circ.holds.user_summary',
4178 Returns a summary of holds statuses for a given user
4182 sub usr_hold_summary {
4183 my($self, $conn, $auth, $user_id) = @_;
4185 my $e = new_editor(authtoken=>$auth);
4186 $e->checkauth or return $e->event;
4187 $e->allowed('VIEW_HOLD') or return $e->event;
4189 my $holds = $e->search_action_hold_request(
4192 fulfillment_time => undef,
4193 cancel_time => undef,
4197 my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
4198 $summary{_hold_status($e, $_)} += 1 for @$holds;
4204 __PACKAGE__->register_method(
4205 method => 'hold_has_copy_at',
4206 api_name => 'open-ils.circ.hold.has_copy_at',
4209 'Returns the ID of the found copy and name of the shelving location if there is ' .
4210 'an available copy at the specified org unit. Returns empty hash otherwise. ' .
4211 'The anticipated use for this method is to determine whether an item is ' .
4212 'available at the library where the user is placing the hold (or, alternatively, '.
4213 'at the pickup library) to encourage bypassing the hold placement and just ' .
4214 'checking out the item.' ,
4216 { desc => 'Authentication Token', type => 'string' },
4217 { desc => 'Method Arguments. Options include: hold_type, hold_target, org_unit. '
4218 . 'hold_type is the hold type code (T, V, C, M, ...). '
4219 . 'hold_target is the identifier of the hold target object. '
4220 . 'org_unit is org unit ID.',
4225 desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
4231 sub hold_has_copy_at {
4232 my($self, $conn, $auth, $args) = @_;
4234 my $e = new_editor(authtoken=>$auth);
4235 $e->checkauth or return $e->event;
4237 my $hold_type = $$args{hold_type};
4238 my $hold_target = $$args{hold_target};
4239 my $org_unit = $$args{org_unit};
4242 select => {acp => ['id'], acpl => ['name']},
4247 filter => { holdable => 't', deleted => 'f' },
4250 ccs => {field => 'id', filter => {holdable => 't', is_available => 't'}, fkey => 'status'}
4253 where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit }},
4257 if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
4259 $query->{where}->{'+acp'}->{id} = $hold_target;
4261 } elsif($hold_type eq 'V') {
4263 $query->{where}->{'+acp'}->{call_number} = $hold_target;
4265 } elsif($hold_type eq 'P') {
4267 $query->{from}->{acp}->{acpm} = {
4268 field => 'target_copy',
4270 filter => {part => $hold_target},
4273 } elsif($hold_type eq 'I') {
4275 $query->{from}->{acp}->{sitem} = {
4278 filter => {issuance => $hold_target},
4281 } elsif($hold_type eq 'T') {
4283 $query->{from}->{acp}->{acn} = {
4285 fkey => 'call_number',
4289 filter => {id => $hold_target},
4297 $query->{from}->{acp}->{acn} = {
4299 fkey => 'call_number',
4308 filter => {metarecord => $hold_target},
4316 my $res = $e->json_query($query)->[0] or return {};
4317 return {copy => $res->{id}, location => $res->{name}} if $res;
4321 # returns true if the user already has an item checked out
4322 # that could be used to fulfill the requested hold.
4323 sub hold_item_is_checked_out {
4324 my($e, $user_id, $hold_type, $hold_target) = @_;
4327 select => {acp => ['id']},
4328 from => {acp => {}},
4332 in => { # copies for circs the user has checked out
4333 select => {circ => ['target_copy']},
4337 checkin_time => undef,
4339 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
4340 {stop_fines => undef}
4350 if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
4352 $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
4354 } elsif($hold_type eq 'V') {
4356 $query->{where}->{'+acp'}->{call_number} = $hold_target;
4358 } elsif($hold_type eq 'P') {
4360 $query->{from}->{acp}->{acpm} = {
4361 field => 'target_copy',
4363 filter => {part => $hold_target},
4366 } elsif($hold_type eq 'I') {
4368 $query->{from}->{acp}->{sitem} = {
4371 filter => {issuance => $hold_target},
4374 } elsif($hold_type eq 'T') {
4376 $query->{from}->{acp}->{acn} = {
4378 fkey => 'call_number',
4382 filter => {id => $hold_target},
4390 $query->{from}->{acp}->{acn} = {
4392 fkey => 'call_number',
4401 filter => {metarecord => $hold_target},
4409 return $e->json_query($query)->[0];
4412 __PACKAGE__->register_method(
4413 method => 'change_hold_title',
4414 api_name => 'open-ils.circ.hold.change_title',
4417 Updates all title level holds targeting the specified bibs to point a new bib./,
4419 { desc => 'Authentication Token', type => 'string' },
4420 { desc => 'New Target Bib Id', type => 'number' },
4421 { desc => 'Old Target Bib Ids', type => 'array' },
4423 return => { desc => '1 on success' }
4427 __PACKAGE__->register_method(
4428 method => 'change_hold_title_for_specific_holds',
4429 api_name => 'open-ils.circ.hold.change_title.specific_holds',
4432 Updates specified holds to target new bib./,
4434 { desc => 'Authentication Token', type => 'string' },
4435 { desc => 'New Target Bib Id', type => 'number' },
4436 { desc => 'Holds Ids for holds to update', type => 'array' },
4438 return => { desc => '1 on success' }
4443 sub change_hold_title {
4444 my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4446 my $e = new_editor(authtoken=>$auth, xact=>1);
4447 return $e->die_event unless $e->checkauth;
4449 my $holds = $e->search_action_hold_request(
4452 capture_time => undef,
4453 cancel_time => undef,
4454 fulfillment_time => undef,
4460 flesh_fields => { ahr => ['usr'] }
4466 for my $hold (@$holds) {
4467 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4468 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4469 $hold->target( $new_bib_id );
4470 $e->update_action_hold_request($hold) or return $e->die_event;
4475 _reset_hold($self, $e->requestor, $_) for @$holds;
4480 sub change_hold_title_for_specific_holds {
4481 my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4483 my $e = new_editor(authtoken=>$auth, xact=>1);
4484 return $e->die_event unless $e->checkauth;
4486 my $holds = $e->search_action_hold_request(
4489 capture_time => undef,
4490 cancel_time => undef,
4491 fulfillment_time => undef,
4497 flesh_fields => { ahr => ['usr'] }
4503 for my $hold (@$holds) {
4504 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4505 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4506 $hold->target( $new_bib_id );
4507 $e->update_action_hold_request($hold) or return $e->die_event;
4512 _reset_hold($self, $e->requestor, $_) for @$holds;
4517 __PACKAGE__->register_method(
4518 method => 'rec_hold_count',
4519 api_name => 'open-ils.circ.bre.holds.count',
4521 desc => q/Returns the total number of holds that target the
4522 selected bib record or its associated copies and call_numbers/,
4524 { desc => 'Bib ID', type => 'number' },
4525 { desc => q/Optional arguments. Supported arguments include:
4526 "pickup_lib_descendant" -> limit holds to those whose pickup
4527 library is equal to or is a child of the provided org unit/,
4531 return => {desc => 'Hold count', type => 'number'}
4535 __PACKAGE__->register_method(
4536 method => 'rec_hold_count',
4537 api_name => 'open-ils.circ.mmr.holds.count',
4539 desc => q/Returns the total number of holds that target the
4540 selected metarecord or its associated copies, call_numbers, and bib records/,
4542 { desc => 'Metarecord ID', type => 'number' },
4544 return => {desc => 'Hold count', type => 'number'}
4548 # XXX Need to add type I holds to these counts
4549 sub rec_hold_count {
4550 my($self, $conn, $target_id, $args) = @_;
4557 filter => {metarecord => $target_id}
4564 filter => { id => $target_id },
4569 if($self->api_name =~ /mmr/) {
4570 delete $bre_join->{bre}->{filter};
4571 $bre_join->{bre}->{join} = $mmr_join;
4577 fkey => 'call_number',
4583 select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4587 cancel_time => undef,
4588 fulfillment_time => undef,
4592 hold_type => [qw/C F R/],
4595 select => {acp => ['id']},
4596 from => { acp => $cn_join }
4606 select => {acn => ['id']},
4607 from => {acn => $bre_join}
4617 select => {bmp => ['id']},
4618 from => {bmp => $bre_join}
4626 target => $target_id
4634 if($self->api_name =~ /mmr/) {
4635 $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4640 select => {bre => ['id']},
4641 from => {bre => $mmr_join}
4647 $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4650 target => $target_id
4656 if (my $pld = $args->{pickup_lib_descendant}) {
4658 my $top_ou = new_editor()->search_actor_org_unit(
4659 {parent_ou => undef}
4660 )->[0]; # XXX Assumes single root node. Not alone in this...
4662 $query->{where}->{'+ahr'}->{pickup_lib} = {
4664 select => {aou => [{
4666 transform => 'actor.org_unit_descendants',
4667 result_field => 'id'
4670 where => {id => $pld}
4672 } if ($pld != $top_ou->id);
4675 # To avoid Internal Server Errors, we get an editor, then run the
4676 # query and check the result. If anything fails, we'll return 0.
4678 if (my $e = new_editor()) {
4679 my $query_result = $e->json_query($query);
4680 if ($query_result && @{$query_result}) {
4681 $result = $query_result->[0]->{count}
4688 # A helper function to calculate a hold's expiration time at a given
4689 # org_unit. Takes the org_unit as an argument and returns either the
4690 # hold expire time as an ISO8601 string or undef if there is no hold
4691 # expiration interval set for the subject ou.
4692 sub calculate_expire_time
4695 my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4697 my $date = DateTime->now->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($interval));
4698 return $U->epoch2ISO8601($date->epoch);
4704 __PACKAGE__->register_method(
4705 method => 'mr_hold_filter_attrs',
4706 api_name => 'open-ils.circ.mmr.holds.filters',
4711 Returns the set of available formats and languages for the
4712 constituent records of the provided metarcord.
4713 If an array of hold IDs is also provided, information about
4714 each is returned as well. This information includes:
4715 1. a slightly easier to read version of holdable_formats
4716 2. attributes describing the set of format icons included
4717 in the set of desired, constituent records.
4720 {desc => 'Metarecord ID', type => 'number'},
4721 {desc => 'Context Org ID', type => 'number'},
4722 {desc => 'Hold ID List', type => 'array'},
4726 Stream of objects. The first will have a 'metarecord' key
4727 containing non-hold-specific metarecord information, subsequent
4728 responses will contain a 'hold' key containing hold-specific
4736 sub mr_hold_filter_attrs {
4737 my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4738 my $e = new_editor();
4740 # by default, return MR / hold attributes for all constituent
4741 # records with holdable copies. If there is a hard boundary,
4742 # though, limit to records with copies within the boundary,
4743 # since anything outside the boundary can never be held.
4746 $org_depth = $U->ou_ancestor_setting_value(
4747 $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4750 # get all org-scoped records w/ holdable copies for this metarecord
4751 my ($bre_ids) = $self->method_lookup(
4752 'open-ils.circ.holds.metarecord.filtered_records')->run(
4753 $mr_id, undef, $org_id, $org_depth);
4755 my $item_lang_attr = 'item_lang'; # configurable?
4756 my $format_attr = $e->retrieve_config_global_flag(
4757 'opac.metarecord.holds.format_attr')->value;
4759 # helper sub for fetching ccvms for a batch of record IDs
4760 sub get_batch_ccvms {
4761 my ($e, $attr, $bre_ids) = @_;
4762 return [] unless $bre_ids and @$bre_ids;
4763 my $vals = $e->search_metabib_record_attr_flat({
4767 return [] unless @$vals;
4768 return $e->search_config_coded_value_map({
4770 code => [map {$_->value} @$vals]
4774 my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4775 my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4780 formats => $formats,
4785 return unless $hold_ids;
4786 my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4787 $icon_attr = $icon_attr ? $icon_attr->value : '';
4789 for my $hold_id (@$hold_ids) {
4790 my $hold = $e->retrieve_action_hold_request($hold_id)
4791 or return $e->event;
4793 next unless $hold->hold_type eq 'M';
4803 # collect the ccvm's for the selected formats / language
4804 # (i.e. the holdable formats) on the MR.
4805 # this assumes a two-key structure for format / language,
4806 # though no assumption is made about the keys themselves.
4807 my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4809 my $format_vals = [];
4810 for my $val (values %$hformats) {
4811 # val is either a single ccvm or an array of them
4812 $val = [$val] unless ref $val eq 'ARRAY';
4813 for my $node (@$val) {
4814 push (@$lang_vals, $node->{_val})
4815 if $node->{_attr} eq $item_lang_attr;
4816 push (@$format_vals, $node->{_val})
4817 if $node->{_attr} eq $format_attr;
4821 # fetch the ccvm's for consistency with the {metarecord} blob
4822 $resp->{hold}{formats} = $e->search_config_coded_value_map({
4823 ctype => $format_attr, code => $format_vals});
4824 $resp->{hold}{langs} = $e->search_config_coded_value_map({
4825 ctype => $item_lang_attr, code => $lang_vals});
4827 # find all of the bib records within this metarcord whose
4828 # format / language match the holdable formats on the hold
4829 my ($bre_ids) = $self->method_lookup(
4830 'open-ils.circ.holds.metarecord.filtered_records')->run(
4831 $hold->target, $hold->holdable_formats,
4832 $hold->selection_ou, $hold->selection_depth);
4834 # now find all of the 'icon' attributes for the records
4835 $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4836 $client->respond($resp);
4842 __PACKAGE__->register_method(
4843 method => "copy_has_holds_count",
4844 api_name => "open-ils.circ.copy.has_holds_count",
4848 Returns the number of holds a paticular copy has
4851 { desc => 'Authentication Token', type => 'string'},
4852 { desc => 'Copy ID', type => 'number'}
4863 sub copy_has_holds_count {
4864 my( $self, $conn, $auth, $copyid ) = @_;
4865 my $e = new_editor(authtoken=>$auth);
4866 return $e->event unless $e->checkauth;
4868 if( $copyid && $copyid > 0 ) {
4869 my $meth = 'retrieve_action_has_holds_count';
4870 my $data = $e->$meth($copyid);
4872 return $data->count();
4878 __PACKAGE__->register_method(
4879 method => "retrieve_holds_by_usr_notify_value_staff",
4880 api_name => "open-ils.circ.holds.retrieve_by_notify_staff",
4882 desc => "Retrieve the hold, for the specified user using the notify value. $ses_is_req_note",
4884 { desc => 'Authentication token', type => 'string' },
4885 { desc => 'User ID', type => 'number' },
4886 { desc => 'notify value', type => 'string' },
4887 { desc => 'notify_type', type => 'string' }
4890 desc => 'Hold objects with transits attached, event on error',
4895 sub retrieve_holds_by_usr_notify_value_staff {
4897 my($self, $conn, $auth, $usr_id, $contact, $cType) = @_;
4899 my $e = new_editor(authtoken=>$auth);
4900 $e->checkauth or return $e->event;
4902 if ($e->requestor->id != $usr_id){
4903 $e->allowed('VIEW_HOLD') or return $e->event;
4907 "select" => { "ahr" => ["id", "sms_notify", "phone_notify", "email_notify", "sms_carrier"]},
4911 "capture_time" => undef,
4912 "cancel_time" => undef,
4913 "fulfillment_time" => undef,
4917 if ($cType eq "day_phone" or $cType eq "evening_phone" or
4918 $cType eq "other_phone" or $cType eq "default_phone"){
4919 $q->{where}->{"-not"} = [
4920 { "phone_notify" => { "=" => $contact} },
4921 { "phone_notify" => { "<>" => undef } }
4926 if ($cType eq "default_sms") {
4927 $q->{where}->{"-not"} = [
4928 { "sms_notify" => { "=" => $contact} },
4929 { "sms_notify" => { "<>" => undef } }
4933 if ($cType eq "default_sms_carrier_id") {
4934 $q->{where}->{"-not"} = [
4935 { "sms_carrier" => { "=" => int($contact)} },
4936 { "sms_carrier" => { "<>" => undef } }
4940 if ($cType =~ /notify/){
4941 # this is was notification pref change
4942 # we find all unfulfilled holds that match have that pref
4943 my $optr = $contact == 1 ? "<>" : "="; # unless it's email, true val means we want to query for not null
4944 my $conj = $optr eq '=' ? '-or' : '-and';
4945 if ($cType =~ /sms/) {
4946 $q->{where}->{$conj} = [ { sms_notify => { $optr => undef } }, { sms_notify => { $optr => '' } } ];
4948 if ($cType =~ /phone/) {
4949 $q->{where}->{$conj} = [ { phone_notify => { $optr => undef } }, { phone_notify => { $optr => '' } } ];
4951 if ($cType =~ /email/) {
4953 $q->{where}->{'+ahr'} = 'email_notify';
4955 $q->{where}->{'-not'} = {'+ahr' => 'email_notify'};
4960 my $holds = $e->json_query($q);
4961 #$hold_ids = [ map { $_->{id} } @$hold_ids ];
4966 __PACKAGE__->register_method(
4967 method => "batch_update_holds_by_value_staff",
4968 api_name => "open-ils.circ.holds.batch_update_holds_by_notify_staff",
4970 desc => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
4972 { desc => 'Authentication token', type => 'string' },
4973 { desc => 'User ID', type => 'number' },
4974 { desc => 'Hold IDs', type => 'array' },
4975 { desc => 'old notify value', type => 'string' },
4976 { desc => 'new notify value', type => 'string' },
4977 { desc => 'field name', type => 'string' },
4978 { desc => 'SMS carrier ID', type => 'number' }
4982 desc => 'Hold objects with transits attached, event on error',
4987 sub batch_update_holds_by_value_staff {
4988 my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType, $carrierId) = @_;
4990 my $e = new_editor(authtoken=>$auth, xact=>1);
4991 $e->checkauth or return $e->event;
4992 if ($e->requestor->id != $usr_id){
4993 $e->allowed('UPDATE_HOLD') or return $e->event;
4997 for my $id (@$hold_ids) {
4999 my $hold = $e->retrieve_action_hold_request($id);
5001 if ($cType eq "day_phone" or $cType eq "evening_phone" or
5002 $cType eq "other_phone" or $cType eq "default_phone") {
5004 if ($newval eq '') {
5005 $hold->clear_phone_notify();
5008 $hold->phone_notify($newval);
5012 if ($cType eq "default_sms"){
5013 if ($newval eq '') {
5014 $hold->clear_sms_notify();
5015 $hold->clear_sms_carrier(); # TODO: prevent orphan sms_carrier, via db trigger
5018 $hold->sms_notify($newval);
5019 $hold->sms_carrier($carrierId);
5024 if ($cType eq "default_sms_carrier_id") {
5025 $hold->sms_carrier($newval);
5028 if ($cType =~ /notify/){
5029 # this is a notification pref change
5030 if ($cType =~ /email/) { $hold->email_notify($newval); }
5031 if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
5032 if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
5033 # the other case, where x_notify is changed to true,
5034 # is covered by an actual value being assigned
5037 $e->update_action_hold_request($hold) or return $e->die_event;
5042 $e->commit; #unless $U->event_code($res);
5048 __PACKAGE__->register_method(
5049 method => "retrieve_holds_by_usr_with_notify",
5050 api_name => "open-ils.circ.holds.retrieve.by_usr.with_notify",
5052 desc => "Retrieve the hold, for the specified user using the notify value. $ses_is_req_note",
5054 { desc => 'Authentication token', type => 'string' },
5055 { desc => 'User ID', type => 'number' },
5058 desc => 'Lists of holds with notification values, event on error',
5063 sub retrieve_holds_by_usr_with_notify {
5065 my($self, $conn, $auth, $usr_id) = @_;
5067 my $e = new_editor(authtoken=>$auth);
5068 $e->checkauth or return $e->event;
5070 if ($e->requestor->id != $usr_id){
5071 $e->allowed('VIEW_HOLD') or return $e->event;
5075 "select" => { "ahr" => ["id", "phone_notify", "email_notify", "sms_carrier", "sms_notify"]},
5079 "capture_time" => undef,
5080 "cancel_time" => undef,
5081 "fulfillment_time" => undef,
5085 my $holds = $e->json_query($q);
5089 __PACKAGE__->register_method(
5090 method => "batch_update_holds_by_value",
5091 api_name => "open-ils.circ.holds.batch_update_holds_by_notify",
5093 desc => "Update a user's specified holds, affected by the contact/notify value change. $ses_is_req_note",
5095 { desc => 'Authentication token', type => 'string' },
5096 { desc => 'User ID', type => 'number' },
5097 { desc => 'Hold IDs', type => 'array' },
5098 { desc => 'old notify value', type => 'string' },
5099 { desc => 'new notify value', type => 'string' },
5100 { desc => 'notify_type', type => 'string' }
5103 desc => 'Hold objects with transits attached, event on error',
5108 sub batch_update_holds_by_value {
5109 my($self, $conn, $auth, $usr_id, $hold_ids, $oldval, $newval, $cType) = @_;
5111 my $e = new_editor(authtoken=>$auth, xact=>1);
5112 $e->checkauth or return $e->event;
5113 if ($e->requestor->id != $usr_id){
5114 $e->allowed('UPDATE_HOLD') or return $e->event;
5118 for my $id (@$hold_ids) {
5120 my $hold = $e->retrieve_action_hold_request(int($id));
5122 if ($cType eq "day_phone" or $cType eq "evening_phone" or
5123 $cType eq "other_phone" or $cType eq "default_phone") {
5124 # change phone number value on hold
5125 $hold->phone_notify($newval);
5127 if ($cType eq "default_sms") {
5128 # change SMS number value on hold
5129 $hold->sms_notify($newval);
5132 if ($cType eq "default_sms_carrier_id") {
5133 $hold->sms_carrier(int($newval));
5136 if ($cType =~ /notify/){
5137 # this is a notification pref change
5138 if ($cType =~ /email/) { $hold->email_notify($newval); }
5139 if ($cType =~ /sms/ and !$newval) { $hold->clear_sms_notify(); }
5140 if ($cType =~ /phone/ and !$newval) { $hold->clear_phone_notify(); }
5141 # the other case, where x_notify is changed to true,
5142 # is covered by an actual value being assigned
5145 $e->update_action_hold_request($hold) or return $e->die_event;
5150 $e->commit; #unless $U->event_code($res);
5154 __PACKAGE__->register_method(
5155 method => "hold_metadata",
5156 api_name => "open-ils.circ.hold.get_metadata",
5161 Returns a stream of objects containing whatever bib,
5162 volume, etc. data is available to the specific hold
5166 {desc => 'Hold Type', type => 'string'},
5167 {desc => 'Hold Target(s)', type => 'number or array'},
5168 {desc => 'Context org unit (optional)', type => 'number'}
5172 Stream of hold metadata objects.
5181 my ($self, $client, $hold_type, $hold_targets, $org_id) = @_;
5183 $hold_targets = [$hold_targets] unless ref $hold_targets;
5185 my $e = new_editor();
5186 for my $target (@$hold_targets) {
5188 # create a dummy hold for find_hold_mvr
5189 my $hold = Fieldmapper::action::hold_request->new;
5190 $hold->hold_type($hold_type);
5191 $hold->target($target);
5193 my (undef, $volume, $copy, $issuance, $part, $bre, $metarecord) =
5194 find_hold_mvr($e, $hold, {suppress_mvr => 1});
5196 $bre->clear_marc; # avoid bulk
5202 issuance => $issuance,
5205 part_required => 'f',
5207 metarecord => $metarecord,
5208 metarecord_filters => {}
5211 # If this is a bib hold or metarecord hold, also return the
5212 # available set of MR filters (AKA "Holdable Formats") for the
5213 # hold. For bib holds these may be used to upgrade the hold
5214 # from a bib to metarecord hold.
5215 if ($hold_type eq 'T') {
5216 my $map = $e->search_metabib_metarecord_source_map(
5217 {source => $meta->{bibrecord}->id})->[0];
5220 $meta->{metarecord} =
5221 $e->retrieve_metabib_metarecord($map->metarecord);
5224 # Also fetch the available parts for bib-level holds.
5225 $meta->{parts} = $e->search_biblio_monograph_part(
5227 {record => $bre->id, deleted => 'f'},
5228 {order_by => {bmp => 'label_sortkey'}}
5232 # T holds on records that have parts are normally OK, but if the record has
5233 # no non-part copies, the hold will ultimately fail. When that happens,
5234 # require the user to select a part.
5236 # If the global flag circ.holds.api_require_monographic_part_when_present is
5237 # enabled, or the library setting circ.holds.ui_require_monographic_part_when_present
5238 # is true for any involved owning_library, then also require part selection.
5239 my $part_required = 0;
5240 if ($meta->{parts}) {
5241 my $part_required_flag = $e->retrieve_config_global_flag('circ.holds.api_require_monographic_part_when_present');
5242 $part_required = ($part_required_flag and $U->is_true($part_required_flag->enabled));
5243 if (!$part_required) {
5244 my $resp = $e->json_query({
5246 acn => ['owning_lib']
5248 from => {acn => {acp => {type => 'left'}}},
5253 {id => undef} # left join
5256 '+acn' => {deleted => 'f', record => $bre->id}
5260 my $org_ids = [map {$_->{owning_lib}} @$resp];
5261 foreach my $org (@$org_ids) { # FIXME: worth shortcutting/optimizing?
5262 if ($U->ou_ancestor_setting_value($org, 'circ.holds.ui_require_monographic_part_when_present')) {
5267 if (!$part_required) {
5268 my $np_copies = $e->json_query({
5269 select => { acp => [{column => 'id', transform => 'count', alias => 'count'}]},
5270 from => {acp => {acn => {}, acpm => {type => 'left'}}},
5272 '+acp' => {deleted => 'f'},
5273 '+acn' => {deleted => 'f', record => $bre->id},
5274 '+acpm' => {id => undef}
5277 $part_required = 1 if $np_copies->[0]->{count} == 0;
5280 $meta->{part_required} = $part_required;
5283 if ($meta->{metarecord}) {
5286 $self->method_lookup('open-ils.circ.mmr.holds.filters')
5287 ->run($meta->{metarecord}->id, $org_id);
5290 $meta->{metarecord_filters} = $filters->{metarecord};
5294 $client->respond($meta);