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 OpenILS::Application::AppUtils;
23 use OpenSRF::EX qw(:try);
27 use OpenSRF::Utils::Logger qw(:logger);
28 use OpenILS::Utils::CStoreEditor q/:funcs/;
29 use OpenILS::Utils::PermitHold;
30 use OpenSRF::Utils::SettingsClient;
31 use OpenILS::Const qw/:const/;
32 use OpenILS::Application::Circ::Transit;
33 use OpenILS::Application::Actor::Friends;
35 use DateTime::Format::ISO8601;
36 use OpenILS::Utils::DateTime qw/:datetime/;
37 use Digest::MD5 qw(md5_hex);
38 use OpenSRF::Utils::Cache;
39 use OpenSRF::Utils::JSON;
40 my $apputils = "OpenILS::Application::AppUtils";
43 __PACKAGE__->register_method(
44 method => "test_and_create_hold_batch",
45 api_name => "open-ils.circ.holds.test_and_create.batch",
48 desc => q/This is for batch creating a set of holds where every field is identical except for the targets./,
50 { desc => 'Authentication token', type => 'string' },
51 { 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'},
52 { desc => 'Array of target ids', type => 'array' }
55 desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
60 __PACKAGE__->register_method(
61 method => "test_and_create_hold_batch",
62 api_name => "open-ils.circ.holds.test_and_create.batch.override",
65 desc => '@see open-ils.circ.holds.test_and_create.batch',
70 sub test_and_create_hold_batch {
71 my( $self, $conn, $auth, $params, $target_list, $oargs ) = @_;
74 if ($self->api_name =~ /override/) {
76 $oargs = { all => 1 } unless defined $oargs;
77 $$params{oargs} = $oargs; # for is_possible checking.
80 my $e = new_editor(authtoken=>$auth);
81 return $e->die_event unless $e->checkauth;
82 $$params{'requestor'} = $e->requestor->id;
85 if ($$params{'hold_type'} eq 'T') { $target_field = 'titleid'; }
86 elsif ($$params{'hold_type'} eq 'C') { $target_field = 'copy_id'; }
87 elsif ($$params{'hold_type'} eq 'R') { $target_field = 'copy_id'; }
88 elsif ($$params{'hold_type'} eq 'F') { $target_field = 'copy_id'; }
89 elsif ($$params{'hold_type'} eq 'I') { $target_field = 'issuanceid'; }
90 elsif ($$params{'hold_type'} eq 'V') { $target_field = 'volume_id'; }
91 elsif ($$params{'hold_type'} eq 'M') { $target_field = 'mrid'; }
92 elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
93 else { return undef; }
95 my $formats_map = delete $$params{holdable_formats_map};
97 foreach (@$target_list) {
98 $$params{$target_field} = $_;
100 # copy the requested formats from the target->formats map
101 # into the top-level formats attr for each hold
102 $$params{holdable_formats} = $formats_map->{$_};
105 ($res) = $self->method_lookup(
106 'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
107 if ($res->{'success'} == 1) {
109 $params->{'depth'} = $res->{'depth'} if $res->{'depth'};
111 if ($$oargs{honor_user_settings}) {
112 my $recipient = $e->retrieve_actor_user($$params{patronid})
113 or return $e->die_event;
114 my $opac_hold_notify = $e->search_actor_user_setting(
115 {usr => $$params{patronid}, name => 'opac.hold_notify'})->[0];
116 if ($opac_hold_notify) {
117 if ($opac_hold_notify->value =~ 'email') {
118 $$params{email_notify} = 1;
120 if ($opac_hold_notify->value =~ 'phone') {
121 my $opac_default_phone = $e->search_actor_user_setting(
122 {usr => $$params{patronid}, name => 'opac.default_phone'})->[0];
123 # FIXME - what's up with the ->value putting quotes around the string?
124 if ($opac_default_phone && $opac_default_phone->value =~ /^"(.*)"$/) {
125 $$params{phone_notify} = $1;
128 if ($opac_hold_notify->value =~ 'sms') {
129 my $opac_default_sms_carrier = $e->search_actor_user_setting(
130 {usr => $$params{patronid}, name => 'opac.default_sms_carrier'})->[0];
131 $$params{sms_carrier} = $opac_default_sms_carrier->value if $opac_default_sms_carrier;
132 my $opac_default_sms_notify = $e->search_actor_user_setting(
133 {usr => $$params{patronid}, name => 'opac.default_sms_notify'})->[0];
134 if ($opac_default_sms_notify && $opac_default_sms_notify->value =~ /^"(.*)"$/) {
135 $$params{sms_notify} = $1;
141 # Remove oargs from params so holds can be created.
142 if ($$params{oargs}) {
143 delete $$params{oargs};
146 my $ahr = construct_hold_request_object($params);
147 my ($res2) = $self->method_lookup(
149 ? 'open-ils.circ.holds.create.override'
150 : 'open-ils.circ.holds.create'
151 )->run($auth, $ahr, $oargs);
153 'target' => $$params{$target_field},
156 $conn->respond($res2);
159 'target' => $$params{$target_field},
162 $conn->respond($res);
168 sub construct_hold_request_object {
171 my $ahr = Fieldmapper::action::hold_request->new;
174 foreach my $field (keys %{ $params }) {
175 if ($field eq 'depth') { $ahr->selection_depth($$params{$field}); }
176 elsif ($field eq 'patronid') {
177 $ahr->usr($$params{$field}); }
178 elsif ($field eq 'titleid') { $ahr->target($$params{$field}); }
179 elsif ($field eq 'copy_id') { $ahr->target($$params{$field}); }
180 elsif ($field eq 'issuanceid') { $ahr->target($$params{$field}); }
181 elsif ($field eq 'volume_id') { $ahr->target($$params{$field}); }
182 elsif ($field eq 'mrid') { $ahr->target($$params{$field}); }
183 elsif ($field eq 'partid') { $ahr->target($$params{$field}); }
185 $ahr->$field($$params{$field});
191 __PACKAGE__->register_method(
192 method => "create_hold_batch",
193 api_name => "open-ils.circ.holds.create.batch",
196 desc => q/@see open-ils.circ.holds.create.batch/,
198 { desc => 'Authentication token', type => 'string' },
199 { desc => 'Array of hold objects', type => 'array' }
202 desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
207 __PACKAGE__->register_method(
208 method => "create_hold_batch",
209 api_name => "open-ils.circ.holds.create.override.batch",
212 desc => '@see open-ils.circ.holds.create.batch',
217 sub create_hold_batch {
218 my( $self, $conn, $auth, $hold_list, $oargs ) = @_;
219 (my $method = $self->api_name) =~ s/\.batch//og;
220 foreach (@$hold_list) {
221 my ($res) = $self->method_lookup($method)->run($auth, $_, $oargs);
222 $conn->respond($res);
228 __PACKAGE__->register_method(
229 method => "create_hold",
230 api_name => "open-ils.circ.holds.create",
232 desc => "Create a new hold for an item. From a permissions perspective, " .
233 "the login session is used as the 'requestor' of the hold. " .
234 "The hold recipient is determined by the 'usr' setting within the hold object. " .
235 'First we verify the requestor has holds request permissions. ' .
236 'Then we verify that the recipient is allowed to make the given hold. ' .
237 'If not, we see if the requestor has "override" capabilities. If not, ' .
238 'a permission exception is returned. If permissions allow, we cycle ' .
239 'through the set of holds objects and create. ' .
240 'If the recipient does not have permission to place multiple holds ' .
241 'on a single title and said operation is attempted, a permission ' .
242 'exception is returned',
244 { desc => 'Authentication token', type => 'string' },
245 { desc => 'Hold object for hold to be created',
246 type => 'object', class => 'ahr' }
249 desc => 'New ahr ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
254 __PACKAGE__->register_method(
255 method => "create_hold",
256 api_name => "open-ils.circ.holds.create.override",
257 notes => '@see open-ils.circ.holds.create',
259 desc => "If the recipient is not allowed to receive the requested hold, " .
260 "call this method to attempt the override",
262 { desc => 'Authentication token', type => 'string' },
264 desc => 'Hold object for hold to be created',
265 type => 'object', class => 'ahr'
269 desc => 'New hold (ahr) ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
275 my( $self, $conn, $auth, $hold, $oargs ) = @_;
276 return -1 unless $hold;
277 my $e = new_editor(authtoken=>$auth, xact=>1);
278 return $e->die_event unless $e->checkauth;
281 if ($self->api_name =~ /override/) {
283 $oargs = { all => 1 } unless defined $oargs;
288 my $requestor = $e->requestor;
289 my $recipient = $requestor;
291 if( $requestor->id ne $hold->usr ) {
292 # Make sure the requestor is allowed to place holds for
293 # the recipient if they are not the same people
294 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->die_event;
295 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->die_event;
298 # If the related org setting tells us to, block if patron privs have expired
299 my $expire_setting = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_BLOCK_HOLD_FOR_EXPIRED_PATRON);
300 if ($expire_setting) {
301 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
302 clean_ISO8601($recipient->expire_date));
304 push( @events, OpenILS::Event->new(
305 'PATRON_ACCOUNT_EXPIRED',
306 "payload" => {"fail_part" => "actor.usr.privs_expired"}
307 )) if( CORE::time > $expire->epoch ) ;
310 # Now make sure the recipient is allowed to receive the specified hold
311 my $porg = $recipient->home_ou;
312 my $rid = $e->requestor->id;
313 my $t = $hold->hold_type;
315 # See if a duplicate hold already exists
317 usr => $recipient->id,
319 fulfillment_time => undef,
320 target => $hold->target,
321 cancel_time => undef,
324 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
326 my $existing = $e->search_action_hold_request($sargs);
328 # See if the requestor has the CREATE_DUPLICATE_HOLDS perm.
329 my $can_dup = $e->allowed('CREATE_DUPLICATE_HOLDS', $recipient->home_ou);
330 # How many are allowed.
331 my $num_dups = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_MAX_DUPLICATE_HOLDS, $e) || 0;
332 push( @events, OpenILS::Event->new('HOLD_EXISTS'))
333 unless (($t eq 'T' || $t eq 'M') && $can_dup && scalar(@$existing) < $num_dups);
334 # Note: We check for @$existing < $num_dups because we're adding a hold with this call.
337 my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
338 push( @events, OpenILS::Event->new('HOLD_ITEM_CHECKED_OUT')) if $checked_out;
340 if ( $t eq OILS_HOLD_TYPE_METARECORD ) {
341 return $e->die_event unless $e->allowed('MR_HOLDS', $porg);
342 } elsif ( $t eq OILS_HOLD_TYPE_TITLE ) {
343 return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
344 } elsif ( $t eq OILS_HOLD_TYPE_VOLUME ) {
345 return $e->die_event unless $e->allowed('VOLUME_HOLDS', $porg);
346 } elsif ( $t eq OILS_HOLD_TYPE_MONOPART ) {
347 return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
348 } elsif ( $t eq OILS_HOLD_TYPE_ISSUANCE ) {
349 return $e->die_event unless $e->allowed('ISSUANCE_HOLDS', $porg);
350 } elsif ( $t eq OILS_HOLD_TYPE_COPY ) {
351 return $e->die_event unless $e->allowed('COPY_HOLDS', $porg);
352 } elsif ( $t eq OILS_HOLD_TYPE_FORCE || $t eq OILS_HOLD_TYPE_RECALL ) {
353 my $copy = $e->retrieve_asset_copy($hold->target)
354 or return $e->die_event;
355 if ( $t eq OILS_HOLD_TYPE_FORCE ) {
356 return $e->die_event unless $e->allowed('COPY_HOLDS_FORCE', $copy->circ_lib);
357 } elsif ( $t eq OILS_HOLD_TYPE_RECALL ) {
358 return $e->die_event unless $e->allowed('COPY_HOLDS_RECALL', $copy->circ_lib);
367 for my $evt (@events) {
369 my $name = $evt->{textcode};
370 if($oargs->{all} || grep { $_ eq $name } @{$oargs->{events}}) {
371 return $e->die_event unless $e->allowed("$name.override", $porg);
379 # Check for hold expiration in the past, and set it to empty string.
380 $hold->expire_time(undef) if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1);
382 # set the configured expire time
383 unless($hold->expire_time || $U->is_true($hold->frozen)) {
384 $hold->expire_time(calculate_expire_time($recipient->home_ou));
388 # if behind-the-desk pickup is supported at the hold pickup lib,
389 # set the value to the patron default, unless a value has already
390 # been applied. If it's not supported, force the value to false.
392 my $bdous = $U->ou_ancestor_setting_value(
394 'circ.holds.behind_desk_pickup_supported', $e);
397 if (!defined $hold->behind_desk) {
399 my $set = $e->search_actor_user_setting({
401 name => 'circ.holds_behind_desk'
404 $hold->behind_desk('t') if $set and
405 OpenSRF::Utils::JSON->JSON2perl($set->value);
408 # behind the desk not supported, force it to false
409 $hold->behind_desk('f');
412 $hold->requestor($e->requestor->id);
413 $hold->request_lib($e->requestor->ws_ou);
414 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
415 $hold = $e->create_action_hold_request($hold) or return $e->die_event;
419 $conn->respond_complete($hold->id);
421 $U->simplereq('open-ils.hold-targeter',
422 'open-ils.hold-targeter.target', {hold => $hold->id}
423 ) unless $U->is_true($hold->frozen);
428 # makes sure that a user has permission to place the type of requested hold
429 # returns the Perm exception if not allowed, returns undef if all is well
430 sub _check_holds_perm {
431 my($type, $user_id, $org_id) = @_;
435 $evt = $apputils->check_perms($user_id, $org_id, "MR_HOLDS" );
436 } elsif ($type eq "T") {
437 $evt = $apputils->check_perms($user_id, $org_id, "TITLE_HOLDS" );
438 } elsif($type eq "V") {
439 $evt = $apputils->check_perms($user_id, $org_id, "VOLUME_HOLDS");
440 } elsif($type eq "C") {
441 $evt = $apputils->check_perms($user_id, $org_id, "COPY_HOLDS" );
448 # tests if the given user is allowed to place holds on another's behalf
449 sub _check_request_holds_perm {
452 if (my $evt = $apputils->check_perms(
453 $user_id, $org_id, "REQUEST_HOLDS")) {
458 my $ses_is_req_note = 'The login session is the requestor. If the requestor is different from the user, ' .
459 'then the requestor must have VIEW_HOLD permissions';
461 __PACKAGE__->register_method(
462 method => "retrieve_holds_by_id",
463 api_name => "open-ils.circ.holds.retrieve_by_id",
465 desc => "Retrieve the hold, with hold transits attached, for the specified ID. $ses_is_req_note",
467 { desc => 'Authentication token', type => 'string' },
468 { desc => 'Hold ID', type => 'number' }
471 desc => 'Hold object with transits attached, event on error',
477 sub retrieve_holds_by_id {
478 my($self, $client, $auth, $hold_id) = @_;
479 my $e = new_editor(authtoken=>$auth);
480 $e->checkauth or return $e->event;
481 $e->allowed('VIEW_HOLD') or return $e->event;
483 my $holds = $e->search_action_hold_request(
485 { id => $hold_id , fulfillment_time => undef },
487 order_by => { ahr => "request_time" },
489 flesh_fields => {ahr => ['notes']}
494 flesh_hold_transits($holds);
495 flesh_hold_notices($holds, $e);
500 __PACKAGE__->register_method(
501 method => "retrieve_holds",
502 api_name => "open-ils.circ.holds.retrieve",
504 desc => "Retrieves all the holds, with hold transits attached, for the specified user. $ses_is_req_note",
506 { desc => 'Authentication token', type => 'string' },
507 { desc => 'User ID', type => 'integer' },
508 { desc => 'Available Only', type => 'boolean' }
511 desc => 'list of holds, event on error',
516 __PACKAGE__->register_method(
517 method => "retrieve_holds",
518 api_name => "open-ils.circ.holds.id_list.retrieve",
521 desc => "Retrieves all the hold IDs, for the specified user. $ses_is_req_note",
523 { desc => 'Authentication token', type => 'string' },
524 { desc => 'User ID', type => 'integer' },
525 { desc => 'Available Only', type => 'boolean' }
528 desc => 'list of holds, event on error',
533 __PACKAGE__->register_method(
534 method => "retrieve_holds",
535 api_name => "open-ils.circ.holds.canceled.retrieve",
538 desc => "Retrieves all the cancelled holds for the specified user. $ses_is_req_note",
540 { desc => 'Authentication token', type => 'string' },
541 { desc => 'User ID', type => 'integer' }
544 desc => 'list of holds, event on error',
549 __PACKAGE__->register_method(
550 method => "retrieve_holds",
551 api_name => "open-ils.circ.holds.canceled.id_list.retrieve",
554 desc => "Retrieves list of cancelled hold IDs for the specified user. $ses_is_req_note",
556 { desc => 'Authentication token', type => 'string' },
557 { desc => 'User ID', type => 'integer' }
560 desc => 'list of hold IDs, event on error',
567 my ($self, $client, $auth, $user_id, $available) = @_;
569 my $e = new_editor(authtoken=>$auth);
570 return $e->event unless $e->checkauth;
571 $user_id = $e->requestor->id unless defined $user_id;
573 my $notes_filter = {staff => 'f'};
574 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
575 unless($user_id == $e->requestor->id) {
576 if($e->allowed('VIEW_HOLD', $user->home_ou)) {
577 $notes_filter = {staff => 't'}
579 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
580 $e, $user_id, $e->requestor->id, 'hold.view');
581 return $e->event unless $allowed;
584 # staff member looking at his/her own holds can see staff and non-staff notes
585 $notes_filter = {} if $e->allowed('VIEW_HOLD', $user->home_ou);
589 select => {ahr => ['id']},
591 where => {usr => $user_id, fulfillment_time => undef}
594 if($self->api_name =~ /canceled/) {
596 # Fetch the canceled holds
597 # order cancelled holds by cancel time, most recent first
599 $holds_query->{order_by} = [{class => 'ahr', field => 'cancel_time', direction => 'desc'}];
602 my $cancel_count = $U->ou_ancestor_setting_value(
603 $e->requestor->ws_ou, 'circ.holds.canceled.display_count', $e);
605 unless($cancel_count) {
606 $cancel_age = $U->ou_ancestor_setting_value(
607 $e->requestor->ws_ou, 'circ.holds.canceled.display_age', $e);
609 # if no settings are defined, default to last 10 cancelled holds
610 $cancel_count = 10 unless $cancel_age;
613 if($cancel_count) { # limit by count
615 $holds_query->{where}->{cancel_time} = {'!=' => undef};
616 $holds_query->{limit} = $cancel_count;
618 } elsif($cancel_age) { # limit by age
620 # find all of the canceled holds that were canceled within the configured time frame
621 my $date = DateTime->now->subtract(seconds => OpenILS::Utils::DateTime->interval_to_seconds($cancel_age));
622 $date = $U->epoch2ISO8601($date->epoch);
623 $holds_query->{where}->{cancel_time} = {'>=' => $date};
628 # order non-cancelled holds by ready-for-pickup, then active, followed by suspended
629 # "compare" sorts false values to the front. testing pickup_lib != current_shelf_lib
630 # will sort by pl = csl > pl != csl > followed by csl is null;
631 $holds_query->{order_by} = [
633 field => 'pickup_lib',
634 compare => {'!=' => {'+ahr' => 'current_shelf_lib'}}},
635 {class => 'ahr', field => 'shelf_time'},
636 {class => 'ahr', field => 'frozen'},
637 {class => 'ahr', field => 'request_time'}
640 $holds_query->{where}->{cancel_time} = undef;
642 $holds_query->{where}->{shelf_time} = {'!=' => undef};
644 $holds_query->{where}->{pickup_lib} = {'=' => {'+ahr' => 'current_shelf_lib'}};
648 my $hold_ids = $e->json_query($holds_query);
649 $hold_ids = [ map { $_->{id} } @$hold_ids ];
651 return $hold_ids if $self->api_name =~ /id_list/;
654 for my $hold_id ( @$hold_ids ) {
656 my $hold = $e->retrieve_action_hold_request($hold_id);
657 $hold->notes($e->search_action_hold_request_note({hold => $hold_id, %$notes_filter}));
660 $e->search_action_hold_transit_copy([
661 {hold => $hold->id, cancel_time => undef},
662 {order_by => {ahtc => 'source_send_time desc'}, limit => 1}])->[0]
672 __PACKAGE__->register_method(
673 method => 'user_hold_count',
674 api_name => 'open-ils.circ.hold.user.count'
677 sub user_hold_count {
678 my ( $self, $conn, $auth, $userid ) = @_;
679 my $e = new_editor( authtoken => $auth );
680 return $e->event unless $e->checkauth;
681 my $patron = $e->retrieve_actor_user($userid)
683 return $e->event unless $e->allowed( 'VIEW_HOLD', $patron->home_ou );
684 return __user_hold_count( $self, $e, $userid );
687 sub __user_hold_count {
688 my ( $self, $e, $userid ) = @_;
689 my $holds = $e->search_action_hold_request(
692 fulfillment_time => undef,
693 cancel_time => undef,
698 return scalar(@$holds);
702 __PACKAGE__->register_method(
703 method => "retrieve_holds_by_pickup_lib",
704 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
706 "Retrieves all the holds, with hold transits attached, for the specified pickup_ou id."
709 __PACKAGE__->register_method(
710 method => "retrieve_holds_by_pickup_lib",
711 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
712 notes => "Retrieves all the hold ids for the specified pickup_ou id. "
715 sub retrieve_holds_by_pickup_lib {
716 my ($self, $client, $login_session, $ou_id) = @_;
718 #FIXME -- put an appropriate permission check here
719 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
720 # $login_session, $user_id, 'VIEW_HOLD' );
721 #return $evt if $evt;
723 my $holds = $apputils->simplereq(
725 "open-ils.cstore.direct.action.hold_request.search.atomic",
727 pickup_lib => $ou_id ,
728 fulfillment_time => undef,
731 { order_by => { ahr => "request_time" } }
734 if ( ! $self->api_name =~ /id_list/ ) {
735 flesh_hold_transits($holds);
739 return [ map { $_->id } @$holds ];
743 __PACKAGE__->register_method(
744 method => "uncancel_hold",
745 api_name => "open-ils.circ.hold.uncancel"
749 my($self, $client, $auth, $hold_id) = @_;
750 my $e = new_editor(authtoken=>$auth, xact=>1);
751 return $e->die_event unless $e->checkauth;
753 my $hold = $e->retrieve_action_hold_request($hold_id)
754 or return $e->die_event;
755 return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
757 if ($hold->fulfillment_time) {
761 unless ($hold->cancel_time) {
766 # if configured to reset the request time, also reset the expire time
767 if($U->ou_ancestor_setting_value(
768 $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
770 $hold->request_time('now');
771 $hold->expire_time(calculate_expire_time($hold->request_lib));
774 $hold->clear_cancel_time;
775 $hold->clear_cancel_cause;
776 $hold->clear_cancel_note;
777 $hold->clear_shelf_time;
778 $hold->clear_current_copy;
779 $hold->clear_capture_time;
780 $hold->clear_prev_check_time;
781 $hold->clear_shelf_expire_time;
782 $hold->clear_current_shelf_lib;
784 $e->update_action_hold_request($hold) or return $e->die_event;
787 $U->simplereq('open-ils.hold-targeter',
788 'open-ils.hold-targeter.target', {hold => $hold_id});
794 __PACKAGE__->register_method(
795 method => "cancel_hold",
796 api_name => "open-ils.circ.hold.cancel",
798 desc => 'Cancels the specified hold. The login session is the requestor. If the requestor is different from the usr field ' .
799 'on the hold, the requestor must have CANCEL_HOLDS permissions. The hold may be either the hold object or the hold id',
801 {desc => 'Authentication token', type => 'string'},
802 {desc => 'Hold ID', type => 'number'},
803 {desc => 'Cause of Cancellation', type => 'string'},
804 {desc => 'Note', type => 'string'}
807 desc => '1 on success, event on error'
813 my($self, $client, $auth, $holdid, $cause, $note) = @_;
815 my $e = new_editor(authtoken=>$auth, xact=>1);
816 return $e->die_event unless $e->checkauth;
818 my $hold = $e->retrieve_action_hold_request($holdid)
819 or return $e->die_event;
821 if( $e->requestor->id ne $hold->usr ) {
822 return $e->die_event unless $e->allowed('CANCEL_HOLDS');
825 if ($hold->cancel_time) {
830 # If the hold is captured, reset the copy status
831 if( $hold->capture_time and $hold->current_copy ) {
833 my $copy = $e->retrieve_asset_copy($hold->current_copy)
834 or return $e->die_event;
836 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
837 $logger->info("canceling hold $holdid whose item is on the holds shelf");
838 # $logger->info("setting copy to status 'reshelving' on hold cancel");
839 # $copy->status(OILS_COPY_STATUS_RESHELVING);
840 # $copy->editor($e->requestor->id);
841 # $copy->edit_date('now');
842 # $e->update_asset_copy($copy) or return $e->event;
844 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
847 $logger->warn("! canceling hold [$hid] that is in transit");
848 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
851 my $trans = $e->retrieve_action_transit_copy($transid);
852 # Leave the transit alive, but set the copy status to
853 # reshelving so it will be properly reshelved when it gets back home
855 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
856 $e->update_action_transit_copy($trans) or return $e->die_event;
862 $hold->cancel_time('now');
863 $hold->cancel_cause($cause);
864 $hold->cancel_note($note);
865 $e->update_action_hold_request($hold)
866 or return $e->die_event;
870 # re-fetch the hold to pick up the real cancel_time (not "now") for A/T
872 $hold = $e->retrieve_action_hold_request($hold->id) or return $e->die_event;
875 if ($e->requestor->id == $hold->usr) {
876 $U->create_events_for_hook('hold_request.cancel.patron', $hold, $hold->pickup_lib);
878 $U->create_events_for_hook('hold_request.cancel.staff', $hold, $hold->pickup_lib);
884 my $update_hold_desc = 'The login session is the requestor. ' .
885 'If the requestor is different from the usr field on the hold, ' .
886 'the requestor must have UPDATE_HOLDS permissions. ' .
887 'If supplying a hash of hold data, "id" must be included. ' .
888 'The hash is ignored if a hold object is supplied, ' .
889 'so you should supply only one kind of hold data argument.' ;
891 __PACKAGE__->register_method(
892 method => "update_hold",
893 api_name => "open-ils.circ.hold.update",
895 desc => "Updates the specified hold. $update_hold_desc",
897 {desc => 'Authentication token', type => 'string'},
898 {desc => 'Hold Object', type => 'object'},
899 {desc => 'Hash of values to be applied', type => 'object'}
902 desc => 'Hold ID on success, event on error',
908 __PACKAGE__->register_method(
909 method => "batch_update_hold",
910 api_name => "open-ils.circ.hold.update.batch",
913 desc => "Updates the specified hold(s). $update_hold_desc",
915 {desc => 'Authentication token', type => 'string'},
916 {desc => 'Array of hold obejcts', type => 'array' },
917 {desc => 'Array of hashes of values to be applied', type => 'array' }
920 desc => 'Hold ID per success, event per error',
926 my($self, $client, $auth, $hold, $values) = @_;
927 my $e = new_editor(authtoken=>$auth, xact=>1);
928 return $e->die_event unless $e->checkauth;
929 my $resp = update_hold_impl($self, $e, $hold, $values);
930 if ($U->event_code($resp)) {
934 $e->commit; # FIXME: update_hold_impl already does $e->commit ??
938 sub batch_update_hold {
939 my($self, $client, $auth, $hold_list, $values_list) = @_;
940 my $e = new_editor(authtoken=>$auth);
941 return $e->die_event unless $e->checkauth;
943 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.
945 $values_list ||= []; # FIXME: either move this above $count declaration, or send an event if both lists undef. Probably the latter.
947 # FIXME: Failing over to [] guarantees warnings for "Use of unitialized value" in update_hold_impl call.
948 # FIXME: We should be sure we only call update_hold_impl with hold object OR hash, not both.
950 for my $idx (0..$count-1) {
952 my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
953 $e->xact_commit unless $U->event_code($resp);
954 $client->respond($resp);
958 return undef; # not in the register return type, assuming we should always have at least one list populated
961 sub update_hold_impl {
962 my($self, $e, $hold, $values) = @_;
964 my $need_retarget = 0;
967 $hold = $e->retrieve_action_hold_request($values->{id})
968 or return $e->die_event;
969 for my $k (keys %$values) {
970 # Outside of pickup_lib (covered by the first regex) I don't know when these would currently change.
971 # But hey, why not cover things that may happen later?
972 if ($k =~ '_(lib|ou)$' || $k eq 'target' || $k eq 'hold_type' || $k eq 'requestor' || $k eq 'selection_depth' || $k eq 'holdable_formats') {
973 if (defined $values->{$k} && defined $hold->$k() && $values->{$k} ne $hold->$k()) {
974 # Value changed? RETARGET!
976 } elsif (defined $hold->$k() != defined $values->{$k}) {
977 # Value being set or cleared? RETARGET!
981 if (defined $values->{$k}) {
982 $hold->$k($values->{$k});
984 my $f = "clear_$k"; $hold->$f();
989 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
990 or return $e->die_event;
992 # don't allow the user to be changed
993 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
995 if($hold->usr ne $e->requestor->id) {
996 # if the hold is for a different user, make sure the
997 # requestor has the appropriate permissions
998 my $usr = $e->retrieve_actor_user($hold->usr)
999 or return $e->die_event;
1000 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1004 # --------------------------------------------------------------
1005 # Changing the request time is like playing God
1006 # --------------------------------------------------------------
1007 if($hold->request_time ne $orig_hold->request_time) {
1008 return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
1009 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
1013 # --------------------------------------------------------------
1014 # Code for making sure staff have appropriate permissons for cut_in_line
1015 # This, as is, doesn't prevent a user from cutting their own holds in line
1017 # --------------------------------------------------------------
1018 if($U->is_true($hold->cut_in_line) ne $U->is_true($orig_hold->cut_in_line)) {
1019 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
1023 # --------------------------------------------------------------
1024 # Disallow hold suspencion if the hold is already captured.
1025 # --------------------------------------------------------------
1026 if ($U->is_true($hold->frozen) and not $U->is_true($orig_hold->frozen)) {
1027 $hold_status = _hold_status($e, $hold);
1028 if ($hold_status > 2 && $hold_status != 7) { # hold is captured
1029 $logger->info("bypassing hold freeze on captured hold");
1030 return OpenILS::Event->new('HOLD_SUSPEND_AFTER_CAPTURE');
1035 # --------------------------------------------------------------
1036 # if the hold is on the holds shelf or in transit and the pickup
1037 # lib changes we need to create a new transit.
1038 # --------------------------------------------------------------
1039 if($orig_hold->pickup_lib ne $hold->pickup_lib) {
1041 $hold_status = _hold_status($e, $hold) unless $hold_status;
1043 if($hold_status == 3) { # in transit
1045 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
1046 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
1048 $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
1050 # update the transit to reflect the new pickup location
1051 my $transit = $e->search_action_hold_transit_copy(
1052 {hold=>$hold->id, cancel_time => undef, dest_recv_time => undef})->[0]
1053 or return $e->die_event;
1055 $transit->prev_dest($transit->dest); # mark the previous destination on the transit
1056 $transit->dest($hold->pickup_lib);
1057 $e->update_action_hold_transit_copy($transit) or return $e->die_event;
1059 } elsif($hold_status == 4 or $hold_status == 5 or $hold_status == 8) { # on holds shelf
1061 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
1062 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
1064 $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
1066 if ($hold->pickup_lib eq $orig_hold->current_shelf_lib) {
1067 # This can happen if the pickup lib is changed while the hold is
1068 # on the shelf, then changed back to the original pickup lib.
1069 # Restore the original shelf_expire_time to prevent abuse.
1070 set_hold_shelf_expire_time(undef, $hold, $e, $hold->shelf_time);
1073 # clear to prevent premature shelf expiration
1074 $hold->clear_shelf_expire_time;
1079 if($U->is_true($hold->frozen)) {
1080 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1081 $hold->clear_current_copy;
1082 $hold->clear_prev_check_time;
1083 # Clear expire_time to prevent frozen holds from expiring.
1084 $logger->info("clearing expire_time for frozen hold ".$hold->id);
1085 $hold->clear_expire_time;
1088 # If the hold_expire_time is in the past && is not equal to the
1089 # original expire_time, then reset the expire time to be in the
1091 if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1 && $U->datecmp($hold->expire_time, $orig_hold->expire_time) != 0) {
1092 $hold->expire_time(calculate_expire_time($hold->request_lib));
1095 # If the hold is reactivated, reset the expire_time.
1096 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1097 $logger->info("Reset expire_time on activated hold ".$hold->id);
1098 $hold->expire_time(calculate_expire_time($hold->request_lib));
1101 $e->update_action_hold_request($hold) or return $e->die_event;
1104 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1105 $logger->info("Running targeter on activated hold ".$hold->id);
1106 $U->simplereq('open-ils.hold-targeter',
1107 'open-ils.hold-targeter.target', {hold => $hold->id});
1110 # a change to mint-condition changes the set of potential copies, so retarget the hold;
1111 if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
1112 _reset_hold($self, $e->requestor, $hold)
1113 } elsif($need_retarget && !defined $hold->capture_time()) { # If needed, retarget the hold due to changes
1114 $U->simplereq('open-ils.hold-targeter',
1115 'open-ils.hold-targeter.target', {hold => $hold->id});
1121 # this does not update the hold in the DB. It only
1122 # sets the shelf_expire_time field on the hold object.
1123 # start_time is optional and defaults to 'now'
1124 sub set_hold_shelf_expire_time {
1125 my ($class, $hold, $editor, $start_time) = @_;
1127 my $shelf_expire = $U->ou_ancestor_setting_value(
1129 'circ.holds.default_shelf_expire_interval',
1133 return undef unless $shelf_expire;
1135 $start_time = ($start_time) ?
1136 DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time)) :
1137 DateTime->now(time_zone => 'local'); # without time_zone we get UTC ... yuck!
1139 my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($shelf_expire);
1140 my $expire_time = $start_time->add(seconds => $seconds);
1142 # if the shelf expire time overlaps with a pickup lib's
1143 # closed date, push it out to the first open date
1144 my $dateinfo = $U->storagereq(
1145 'open-ils.storage.actor.org_unit.closed_date.overlap',
1146 $hold->pickup_lib, $expire_time->strftime('%FT%T%z'));
1149 my $dt_parser = DateTime::Format::ISO8601->new;
1150 $expire_time = $dt_parser->parse_datetime(clean_ISO8601($dateinfo->{end}));
1152 # TODO: enable/disable time bump via setting?
1153 $expire_time->set(hour => '23', minute => '59', second => '59');
1155 $logger->info("circulator: shelf_expire_time overlaps".
1156 " with closed date, pushing expire time to $expire_time");
1159 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
1165 my($e, $orig_hold, $hold, $copy) = @_;
1166 my $src = $orig_hold->pickup_lib;
1167 my $dest = $hold->pickup_lib;
1169 $logger->info("putting hold into transit on pickup_lib update");
1171 my $transit = Fieldmapper::action::hold_transit_copy->new;
1172 $transit->hold($hold->id);
1173 $transit->source($src);
1174 $transit->dest($dest);
1175 $transit->target_copy($copy->id);
1176 $transit->source_send_time('now');
1177 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1179 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1180 $copy->editor($e->requestor->id);
1181 $copy->edit_date('now');
1183 $e->create_action_hold_transit_copy($transit) or return $e->die_event;
1184 $e->update_asset_copy($copy) or return $e->die_event;
1188 # if the hold is frozen, this method ensures that the hold is not "targeted",
1189 # that is, it clears the current_copy and prev_check_time to essentiallly
1190 # reset the hold. If it is being activated, it runs the targeter in the background
1191 sub update_hold_if_frozen {
1192 my($self, $e, $hold, $orig_hold) = @_;
1193 return if $hold->capture_time;
1195 if($U->is_true($hold->frozen)) {
1196 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1197 $hold->clear_current_copy;
1198 $hold->clear_prev_check_time;
1201 if($U->is_true($orig_hold->frozen)) {
1202 $logger->info("Running targeter on activated hold ".$hold->id);
1203 $U->simplereq('open-ils.hold-targeter',
1204 'open-ils.hold-targeter.target', {hold => $hold->id});
1209 __PACKAGE__->register_method(
1210 method => "hold_note_CUD",
1211 api_name => "open-ils.circ.hold_request.note.cud",
1213 desc => 'Create, update or delete a hold request note. If the operator (from Auth. token) '
1214 . 'is not the owner of the hold, the UPDATE_HOLD permission is required',
1216 { desc => 'Authentication token', type => 'string' },
1217 { desc => 'Hold note object', type => 'object' }
1220 desc => 'Returns the note ID, event on error'
1226 my($self, $conn, $auth, $note) = @_;
1228 my $e = new_editor(authtoken => $auth, xact => 1);
1229 return $e->die_event unless $e->checkauth;
1231 my $hold = $e->retrieve_action_hold_request($note->hold)
1232 or return $e->die_event;
1234 if($hold->usr ne $e->requestor->id) {
1235 my $usr = $e->retrieve_actor_user($hold->usr);
1236 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1237 $note->staff('t') if $note->isnew;
1241 $e->create_action_hold_request_note($note) or return $e->die_event;
1242 } elsif($note->ischanged) {
1243 $e->update_action_hold_request_note($note) or return $e->die_event;
1244 } elsif($note->isdeleted) {
1245 $e->delete_action_hold_request_note($note) or return $e->die_event;
1253 __PACKAGE__->register_method(
1254 method => "retrieve_hold_status",
1255 api_name => "open-ils.circ.hold.status.retrieve",
1257 desc => 'Calculates the current status of the hold. The requestor must have ' .
1258 'VIEW_HOLD permissions if the hold is for a user other than the requestor' ,
1260 { desc => 'Hold ID', type => 'number' }
1263 # type => 'number', # event sometimes
1264 desc => <<'END_OF_DESC'
1265 Returns event on error or:
1266 -1 on error (for now),
1267 1 for 'waiting for copy to become available',
1268 2 for 'waiting for copy capture',
1271 5 for 'hold-shelf-delay'
1274 8 for 'captured, on wrong hold shelf'
1281 sub retrieve_hold_status {
1282 my($self, $client, $auth, $hold_id) = @_;
1284 my $e = new_editor(authtoken => $auth);
1285 return $e->event unless $e->checkauth;
1286 my $hold = $e->retrieve_action_hold_request($hold_id)
1287 or return $e->event;
1289 if( $e->requestor->id != $hold->usr ) {
1290 return $e->event unless $e->allowed('VIEW_HOLD');
1293 return _hold_status($e, $hold);
1299 if ($hold->cancel_time) {
1302 if ($U->is_true($hold->frozen) && !$hold->capture_time) {
1305 if ($hold->current_shelf_lib and $hold->current_shelf_lib ne $hold->pickup_lib) {
1308 if ($hold->fulfillment_time) {
1311 return 1 unless $hold->current_copy;
1312 return 2 unless $hold->capture_time;
1314 my $copy = $hold->current_copy;
1315 unless( ref $copy ) {
1316 $copy = $e->retrieve_asset_copy($hold->current_copy)
1317 or return $e->event;
1320 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
1322 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
1324 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
1325 return 4 unless $hs_wait_interval;
1327 # if a hold_shelf_status_delay interval is defined and start_time plus
1328 # the interval is greater than now, consider the hold to be in the virtual
1329 # "on its way to the holds shelf" status. Return 5.
1331 my $transit = $e->search_action_hold_transit_copy({
1333 target_copy => $copy->id,
1334 cancel_time => undef,
1335 dest_recv_time => {'!=' => undef},
1337 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
1338 $start_time = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time));
1339 my $end_time = $start_time->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($hs_wait_interval));
1341 return 5 if $end_time > DateTime->now;
1350 __PACKAGE__->register_method(
1351 method => "retrieve_hold_queue_stats",
1352 api_name => "open-ils.circ.hold.queue_stats.retrieve",
1354 desc => 'Returns summary data about the state of a hold',
1356 { desc => 'Authentication token', type => 'string'},
1357 { desc => 'Hold ID', type => 'number'},
1360 desc => q/Summary object with keys:
1361 total_holds : total holds in queue
1362 queue_position : current queue position
1363 potential_copies : number of potential copies for this hold
1364 estimated_wait : estimated wait time in days
1365 status : hold status
1366 -1 => error or unexpected state,
1367 1 => 'waiting for copy to become available',
1368 2 => 'waiting for copy capture',
1371 5 => 'hold-shelf-delay'
1378 sub retrieve_hold_queue_stats {
1379 my($self, $conn, $auth, $hold_id) = @_;
1380 my $e = new_editor(authtoken => $auth);
1381 return $e->event unless $e->checkauth;
1382 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
1383 if($e->requestor->id != $hold->usr) {
1384 return $e->event unless $e->allowed('VIEW_HOLD');
1386 return retrieve_hold_queue_status_impl($e, $hold);
1389 sub retrieve_hold_queue_status_impl {
1393 # The holds queue is defined as the distinct set of holds that share at
1394 # least one potential copy with the context hold, plus any holds that
1395 # share the same hold type and target. The latter part exists to
1396 # accomodate holds that currently have no potential copies
1397 my $q_holds = $e->json_query({
1399 # fetch cut_in_line and request_time since they're in the order_by
1400 # and we're asking for distinct values
1401 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1405 select => { ahcm => ['hold'] },
1410 'field' => 'target_copy',
1411 'fkey' => 'target_copy'
1415 where => { '+ahcm2' => { hold => $hold->id } },
1422 "field" => "cut_in_line",
1423 "transform" => "coalesce",
1425 "direction" => "desc"
1427 { "class" => "ahr", "field" => "request_time" }
1432 if (!@$q_holds) { # none? maybe we don't have a map ...
1433 $q_holds = $e->json_query({
1434 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1439 "field" => "cut_in_line",
1440 "transform" => "coalesce",
1442 "direction" => "desc"
1444 { "class" => "ahr", "field" => "request_time" }
1447 hold_type => $hold->hold_type,
1448 target => $hold->target,
1449 capture_time => undef,
1450 cancel_time => undef,
1452 {expire_time => undef },
1453 {expire_time => {'>' => 'now'}}
1461 for my $h (@$q_holds) {
1462 last if $h->{id} == $hold->id;
1466 my $hold_data = $e->json_query({
1468 acp => [ {column => 'id', transform => 'count', aggregate => 1, alias => 'count'} ],
1469 ccm => [ {column =>'avg_wait_time'} ]
1475 ccm => {type => 'left'}
1480 where => {'+ahcm' => {hold => $hold->id} }
1483 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1485 my $default_wait = $U->ou_ancestor_setting_value(
1486 $user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL, $e);
1487 my $min_wait = $U->ou_ancestor_setting_value(
1488 $user_org, 'circ.holds.min_estimated_wait_interval', $e);
1489 $min_wait = OpenILS::Utils::DateTime->interval_to_seconds($min_wait || '0 seconds');
1490 $default_wait ||= '0 seconds';
1492 # Estimated wait time is the average wait time across the set
1493 # of potential copies, divided by the number of potential copies
1494 # times the queue position.
1496 my $combined_secs = 0;
1497 my $num_potentials = 0;
1499 for my $wait_data (@$hold_data) {
1500 my $count += $wait_data->{count};
1501 $combined_secs += $count *
1502 OpenILS::Utils::DateTime->interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
1503 $num_potentials += $count;
1506 my $estimated_wait = -1;
1508 if($num_potentials) {
1509 my $avg_wait = $combined_secs / $num_potentials;
1510 $estimated_wait = $qpos * ($avg_wait / $num_potentials);
1511 $estimated_wait = $min_wait if $estimated_wait < $min_wait and $estimated_wait != -1;
1515 total_holds => scalar(@$q_holds),
1516 queue_position => $qpos,
1517 potential_copies => $num_potentials,
1518 status => _hold_status( $e, $hold ),
1519 estimated_wait => int($estimated_wait)
1524 sub fetch_open_hold_by_current_copy {
1527 my $hold = $apputils->simplereq(
1529 'open-ils.cstore.direct.action.hold_request.search.atomic',
1530 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1531 return $hold->[0] if ref($hold);
1535 sub fetch_related_holds {
1538 return $apputils->simplereq(
1540 'open-ils.cstore.direct.action.hold_request.search.atomic',
1541 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1545 __PACKAGE__->register_method(
1546 method => "hold_pull_list",
1547 api_name => "open-ils.circ.hold_pull_list.retrieve",
1549 desc => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1550 'The location is determined by the login session.',
1552 { desc => 'Limit (optional)', type => 'number'},
1553 { desc => 'Offset (optional)', type => 'number'},
1556 desc => 'reference to a list of holds, or event on failure',
1561 __PACKAGE__->register_method(
1562 method => "hold_pull_list",
1563 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
1565 desc => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
1566 'The location is determined by the login session.',
1568 { desc => 'Limit (optional)', type => 'number'},
1569 { desc => 'Offset (optional)', type => 'number'},
1572 desc => 'reference to a list of holds, or event on failure',
1577 __PACKAGE__->register_method(
1578 method => "hold_pull_list",
1579 api_name => "open-ils.circ.hold_pull_list.retrieve.count",
1581 desc => 'Returns a count of holds that need to be "pulled" by a given location. ' .
1582 'The location is determined by the login session.',
1584 { desc => 'Limit (optional)', type => 'number'},
1585 { desc => 'Offset (optional)', type => 'number'},
1588 desc => 'Holds count (integer), or event on failure',
1594 __PACKAGE__->register_method(
1595 method => "hold_pull_list",
1597 # TODO: tag with api_level 2 once fully supported
1598 api_name => "open-ils.circ.hold_pull_list.fleshed.stream",
1600 desc => q/Returns a stream of fleshed holds that need to be
1601 "pulled" by a given location. The location is
1602 determined by the login session.
1603 This API calls always run in authoritative mode./,
1605 { desc => 'Limit (optional)', type => 'number'},
1606 { desc => 'Offset (optional)', type => 'number'},
1609 desc => 'Stream of holds holds, or event on failure',
1614 sub hold_pull_list {
1615 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1616 my( $reqr, $evt ) = $U->checkses($authtoken);
1617 return $evt if $evt;
1619 my $org = $reqr->ws_ou || $reqr->home_ou;
1620 # the perm locaiton shouldn't really matter here since holds
1621 # will exist all over and VIEW_HOLDS should be universal
1622 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1623 return $evt if $evt;
1625 if($self->api_name =~ /count/) {
1627 my $count = $U->storagereq(
1628 'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1629 $org, $limit, $offset );
1631 $logger->info("Grabbing pull list for org unit $org with $count items");
1634 } elsif( $self->api_name =~ /id_list/ ) {
1636 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1637 $org, $limit, $offset );
1639 } elsif ($self->api_name =~ /fleshed/) {
1641 my $ids = $U->storagereq(
1642 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1643 $org, $limit, $offset );
1645 my $e = new_editor(xact => 1, requestor => $reqr);
1646 $conn->respond(uber_hold_impl($e, $_, {flesh_acpl => 1})) for @$ids;
1648 $conn->respond_complete;
1653 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1654 $org, $limit, $offset );
1658 __PACKAGE__->register_method(
1659 method => "print_hold_pull_list",
1660 api_name => "open-ils.circ.hold_pull_list.print",
1662 desc => 'Returns an HTML-formatted holds pull list',
1664 { desc => 'Authtoken', type => 'string'},
1665 { desc => 'Org unit ID. Optional, defaults to workstation org unit', type => 'number'},
1668 desc => 'HTML string',
1674 sub print_hold_pull_list {
1675 my($self, $client, $auth, $org_id) = @_;
1677 my $e = new_editor(authtoken=>$auth);
1678 return $e->event unless $e->checkauth;
1680 $org_id = (defined $org_id) ? $org_id : $e->requestor->ws_ou;
1681 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1683 my $hold_ids = $U->storagereq(
1684 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1687 return undef unless @$hold_ids;
1689 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1691 # Holds will /NOT/ be in order after this ...
1692 my $holds = $e->search_action_hold_request({id => $hold_ids}, {substream => 1});
1693 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1695 # ... so we must resort.
1696 my $hold_map = +{map { $_->id => $_ } @$holds};
1697 my $sorted_holds = [];
1698 push @$sorted_holds, $hold_map->{$_} foreach @$hold_ids;
1700 return $U->fire_object_event(
1701 undef, "ahr.format.pull_list", $sorted_holds,
1702 $org_id, undef, undef, $client
1707 __PACKAGE__->register_method(
1708 method => "print_hold_pull_list_stream",
1710 api_name => "open-ils.circ.hold_pull_list.print.stream",
1712 desc => 'Returns a stream of fleshed holds',
1714 { desc => 'Authtoken', type => 'string'},
1715 { 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)',
1720 desc => 'A stream of fleshed holds',
1726 sub print_hold_pull_list_stream {
1727 my($self, $client, $auth, $params) = @_;
1729 my $e = new_editor(authtoken=>$auth);
1730 return $e->die_event unless $e->checkauth;
1732 delete($$params{org_id}) unless (int($$params{org_id}));
1733 delete($$params{limit}) unless (int($$params{limit}));
1734 delete($$params{offset}) unless (int($$params{offset}));
1735 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
1736 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
1737 $$params{chunk_size} ||= 10;
1738 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
1740 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
1741 return $e->die_event unless $e->allowed('VIEW_HOLD', $$params{org_id });
1744 if ($$params{sort} && @{ $$params{sort} }) {
1745 for my $s (@{ $$params{sort} }) {
1746 if ($s eq 'acplo.position') {
1748 "class" => "acplo", "field" => "position",
1749 "transform" => "coalesce", "params" => [999]
1751 } elsif ($s eq 'prefix') {
1752 push @$sort, {"class" => "acnp", "field" => "label_sortkey"};
1753 } elsif ($s eq 'call_number') {
1754 push @$sort, {"class" => "acn", "field" => "label_sortkey"};
1755 } elsif ($s eq 'suffix') {
1756 push @$sort, {"class" => "acns", "field" => "label_sortkey"};
1757 } elsif ($s eq 'request_time') {
1758 push @$sort, {"class" => "ahr", "field" => "request_time"};
1762 push @$sort, {"class" => "ahr", "field" => "request_time"};
1765 my $holds_ids = $e->json_query(
1767 "select" => {"ahr" => ["id"]},
1772 "fkey" => "current_copy",
1774 "circ_lib" => $$params{org_id}, "status" => [0,7]
1779 "fkey" => "call_number",
1793 "fkey" => "circ_lib",
1796 "location" => {"=" => {"+acp" => "location"}}
1805 "capture_time" => undef,
1806 "cancel_time" => undef,
1808 {"expire_time" => undef },
1809 {"expire_time" => {">" => "now"}}
1813 (@$sort ? (order_by => $sort) : ()),
1814 ($$params{limit} ? (limit => $$params{limit}) : ()),
1815 ($$params{offset} ? (offset => $$params{offset}) : ())
1816 }, {"substream" => 1}
1817 ) or return $e->die_event;
1819 $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
1822 for my $hid (@$holds_ids) {
1823 push @chunk, $e->retrieve_action_hold_request([
1827 "ahr" => ["usr", "current_copy"],
1829 "acp" => ["location", "call_number", "parts"],
1830 "acn" => ["record","prefix","suffix"]
1835 if (@chunk >= $$params{chunk_size}) {
1836 $client->respond( \@chunk );
1840 $client->respond_complete( \@chunk ) if (@chunk);
1847 __PACKAGE__->register_method(
1848 method => 'fetch_hold_notify',
1849 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
1852 Returns a list of hold notification objects based on hold id.
1853 @param authtoken The loggin session key
1854 @param holdid The id of the hold whose notifications we want to retrieve
1855 @return An array of hold notification objects, event on error.
1859 sub fetch_hold_notify {
1860 my( $self, $conn, $authtoken, $holdid ) = @_;
1861 my( $requestor, $evt ) = $U->checkses($authtoken);
1862 return $evt if $evt;
1863 my ($hold, $patron);
1864 ($hold, $evt) = $U->fetch_hold($holdid);
1865 return $evt if $evt;
1866 ($patron, $evt) = $U->fetch_user($hold->usr);
1867 return $evt if $evt;
1869 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
1870 return $evt if $evt;
1872 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
1873 return $U->cstorereq(
1874 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
1878 __PACKAGE__->register_method(
1879 method => 'create_hold_notify',
1880 api_name => 'open-ils.circ.hold_notification.create',
1882 Creates a new hold notification object
1883 @param authtoken The login session key
1884 @param notification The hold notification object to create
1885 @return ID of the new object on success, Event on error
1889 sub create_hold_notify {
1890 my( $self, $conn, $auth, $note ) = @_;
1891 my $e = new_editor(authtoken=>$auth, xact=>1);
1892 return $e->die_event unless $e->checkauth;
1894 my $hold = $e->retrieve_action_hold_request($note->hold)
1895 or return $e->die_event;
1896 my $patron = $e->retrieve_actor_user($hold->usr)
1897 or return $e->die_event;
1899 return $e->die_event unless
1900 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
1902 $note->notify_staff($e->requestor->id);
1903 $e->create_action_hold_notification($note) or return $e->die_event;
1908 __PACKAGE__->register_method(
1909 method => 'create_hold_note',
1910 api_name => 'open-ils.circ.hold_note.create',
1912 Creates a new hold request note object
1913 @param authtoken The login session key
1914 @param note The hold note object to create
1915 @return ID of the new object on success, Event on error
1919 sub create_hold_note {
1920 my( $self, $conn, $auth, $note ) = @_;
1921 my $e = new_editor(authtoken=>$auth, xact=>1);
1922 return $e->die_event unless $e->checkauth;
1924 my $hold = $e->retrieve_action_hold_request($note->hold)
1925 or return $e->die_event;
1926 my $patron = $e->retrieve_actor_user($hold->usr)
1927 or return $e->die_event;
1929 return $e->die_event unless
1930 $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
1932 $e->create_action_hold_request_note($note) or return $e->die_event;
1937 __PACKAGE__->register_method(
1938 method => 'reset_hold',
1939 api_name => 'open-ils.circ.hold.reset',
1941 Un-captures and un-targets a hold, essentially returning
1942 it to the state it was in directly after it was placed,
1943 then attempts to re-target the hold
1944 @param authtoken The login session key
1945 @param holdid The id of the hold
1951 my( $self, $conn, $auth, $holdid ) = @_;
1953 my ($hold, $evt) = $U->fetch_hold($holdid);
1954 return $evt if $evt;
1955 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1956 return $evt if $evt;
1957 $evt = _reset_hold($self, $reqr, $hold);
1958 return $evt if $evt;
1963 __PACKAGE__->register_method(
1964 method => 'reset_hold_batch',
1965 api_name => 'open-ils.circ.hold.reset.batch'
1968 sub reset_hold_batch {
1969 my($self, $conn, $auth, $hold_ids) = @_;
1971 my $e = new_editor(authtoken => $auth);
1972 return $e->event unless $e->checkauth;
1974 for my $hold_id ($hold_ids) {
1976 my $hold = $e->retrieve_action_hold_request(
1977 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
1978 or return $e->event;
1980 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1981 _reset_hold($self, $e->requestor, $hold);
1989 my ($self, $reqr, $hold) = @_;
1991 my $e = new_editor(xact =>1, requestor => $reqr);
1993 $logger->info("reseting hold ".$hold->id);
1995 my $hid = $hold->id;
1997 if( $hold->capture_time and $hold->current_copy ) {
1999 my $copy = $e->retrieve_asset_copy($hold->current_copy)
2000 or return $e->die_event;
2002 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
2003 $logger->info("setting copy to status 'reshelving' on hold retarget");
2004 $copy->status(OILS_COPY_STATUS_RESHELVING);
2005 $copy->editor($e->requestor->id);
2006 $copy->edit_date('now');
2007 $e->update_asset_copy($copy) or return $e->die_event;
2009 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
2011 $logger->warn("! reseting hold [$hid] that is in transit");
2012 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
2015 my $trans = $e->retrieve_action_transit_copy($transid);
2017 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
2018 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
2019 $logger->info("Transit abort completed with result $evt");
2020 unless ("$evt" eq 1) {
2029 $hold->clear_capture_time;
2030 $hold->clear_current_copy;
2031 $hold->clear_shelf_time;
2032 $hold->clear_shelf_expire_time;
2033 $hold->clear_current_shelf_lib;
2035 $e->update_action_hold_request($hold) or return $e->die_event;
2038 $U->simplereq('open-ils.hold-targeter',
2039 'open-ils.hold-targeter.target', {hold => $hold->id});
2045 __PACKAGE__->register_method(
2046 method => 'fetch_open_title_holds',
2047 api_name => 'open-ils.circ.open_holds.retrieve',
2049 Returns a list ids of un-fulfilled holds for a given title id
2050 @param authtoken The login session key
2051 @param id the id of the item whose holds we want to retrieve
2052 @param type The hold type - M, T, I, V, C, F, R
2056 sub fetch_open_title_holds {
2057 my( $self, $conn, $auth, $id, $type, $org ) = @_;
2058 my $e = new_editor( authtoken => $auth );
2059 return $e->event unless $e->checkauth;
2062 $org ||= $e->requestor->ws_ou;
2064 # return $e->search_action_hold_request(
2065 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
2067 # XXX make me return IDs in the future ^--
2068 my $holds = $e->search_action_hold_request(
2071 cancel_time => undef,
2073 fulfillment_time => undef
2077 flesh_hold_transits($holds);
2082 sub flesh_hold_transits {
2084 for my $hold ( @$holds ) {
2086 $apputils->simplereq(
2088 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
2089 { hold => $hold->id, cancel_time => undef },
2090 { order_by => { ahtc => 'id desc' }, limit => 1 }
2096 sub flesh_hold_notices {
2097 my( $holds, $e ) = @_;
2098 $e ||= new_editor();
2100 for my $hold (@$holds) {
2101 my $notices = $e->search_action_hold_notification(
2103 { hold => $hold->id },
2104 { order_by => { anh => 'notify_time desc' } },
2109 $hold->notify_count(scalar(@$notices));
2111 my $n = $e->retrieve_action_hold_notification($$notices[0])
2112 or return $e->event;
2113 $hold->notify_time($n->notify_time);
2119 __PACKAGE__->register_method(
2120 method => 'fetch_captured_holds',
2121 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
2125 Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
2126 @param authtoken The login session key
2127 @param org The org id of the location in question
2128 @param match_copy A specific copy to limit to
2132 __PACKAGE__->register_method(
2133 method => 'fetch_captured_holds',
2134 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
2138 Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
2139 @param authtoken The login session key
2140 @param org The org id of the location in question
2141 @param match_copy A specific copy to limit to
2145 __PACKAGE__->register_method(
2146 method => 'fetch_captured_holds',
2147 api_name => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
2151 Returns list ids of shelf-expired un-fulfilled holds for a given title id
2152 @param authtoken The login session key
2153 @param org The org id of the location in question
2154 @param match_copy A specific copy to limit to
2158 __PACKAGE__->register_method(
2159 method => 'fetch_captured_holds',
2161 'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
2165 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2166 for a given shelf lib
2170 __PACKAGE__->register_method(
2171 method => 'fetch_captured_holds',
2173 'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
2177 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2178 for a given shelf lib
2183 sub fetch_captured_holds {
2184 my( $self, $conn, $auth, $org, $match_copy ) = @_;
2186 my $e = new_editor(authtoken => $auth);
2187 return $e->die_event unless $e->checkauth;
2188 return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
2190 $org ||= $e->requestor->ws_ou;
2192 my $current_copy = { '!=' => undef };
2193 $current_copy = { '=' => $match_copy } if $match_copy;
2196 select => { alhr => ['id'] },
2201 fkey => 'current_copy'
2206 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
2208 capture_time => { "!=" => undef },
2209 current_copy => $current_copy,
2210 fulfillment_time => undef,
2211 current_shelf_lib => $org
2215 if($self->api_name =~ /expired/) {
2216 $query->{'where'}->{'+alhr'}->{'-or'} = {
2217 shelf_expire_time => { '<' => 'today'},
2218 cancel_time => { '!=' => undef },
2221 my $hold_ids = $e->json_query( $query );
2223 if ($self->api_name =~ /wrong_shelf/) {
2224 # fetch holds whose current_shelf_lib is $org, but whose pickup
2225 # lib is some other org unit. Ignore already-retrieved holds.
2227 pickup_lib_changed_on_shelf_holds(
2228 $e, $org, [map {$_->{id}} @$hold_ids]);
2229 # match the layout of other items in $hold_ids
2230 push (@$hold_ids, {id => $_}) for @$wrong_shelf;
2234 for my $hold_id (@$hold_ids) {
2235 if($self->api_name =~ /id_list/) {
2236 $conn->respond($hold_id->{id});
2240 $e->retrieve_action_hold_request([
2244 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
2245 order_by => {anh => 'notify_time desc'}
2255 __PACKAGE__->register_method(
2256 method => "print_expired_holds_stream",
2257 api_name => "open-ils.circ.captured_holds.expired.print.stream",
2261 sub print_expired_holds_stream {
2262 my ($self, $client, $auth, $params) = @_;
2264 # No need to check specific permissions: we're going to call another method
2265 # that will do that.
2266 my $e = new_editor("authtoken" => $auth);
2267 return $e->die_event unless $e->checkauth;
2269 delete($$params{org_id}) unless (int($$params{org_id}));
2270 delete($$params{limit}) unless (int($$params{limit}));
2271 delete($$params{offset}) unless (int($$params{offset}));
2272 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2273 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2274 $$params{chunk_size} ||= 10;
2275 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2277 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2279 my @hold_ids = $self->method_lookup(
2280 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
2281 )->run($auth, $params->{"org_id"});
2286 } elsif (defined $U->event_code($hold_ids[0])) {
2288 return $hold_ids[0];
2291 $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
2294 my @hid_chunk = splice @hold_ids, 0, $params->{"chunk_size"};
2296 my $result_chunk = $e->json_query({
2298 "acp" => ["barcode"],
2300 first_given_name second_given_name family_name alias
2309 "field" => "id", "fkey" => "current_copy",
2312 "field" => "id", "fkey" => "call_number",
2315 "field" => "id", "fkey" => "record"
2319 "acpl" => {"field" => "id", "fkey" => "location"}
2322 "au" => {"field" => "id", "fkey" => "usr"}
2325 "where" => {"+ahr" => {"id" => \@hid_chunk}}
2326 }) or return $e->die_event;
2327 $client->respond($result_chunk);
2334 __PACKAGE__->register_method(
2335 method => "check_title_hold_batch",
2336 api_name => "open-ils.circ.title_hold.is_possible.batch",
2339 desc => '@see open-ils.circ.title_hold.is_possible.batch',
2341 { desc => 'Authentication token', type => 'string'},
2342 { desc => 'Array of Hash of named parameters', type => 'array'},
2345 desc => 'Array of response objects',
2351 sub check_title_hold_batch {
2352 my($self, $client, $authtoken, $param_list, $oargs) = @_;
2353 foreach (@$param_list) {
2354 my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
2355 $client->respond($res);
2361 __PACKAGE__->register_method(
2362 method => "check_title_hold",
2363 api_name => "open-ils.circ.title_hold.is_possible",
2365 desc => 'Determines if a hold were to be placed by a given user, ' .
2366 'whether or not said hold would have any potential copies to fulfill it.' .
2367 'The named paramaters of the second argument include: ' .
2368 'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
2369 'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
2371 { desc => 'Authentication token', type => 'string'},
2372 { desc => 'Hash of named parameters', type => 'object'},
2375 desc => 'List of new message IDs (empty if none)',
2381 =head3 check_title_hold (token, hash)
2383 The named fields in the hash are:
2385 patronid - ID of the hold recipient (required)
2386 depth - hold range depth (default 0)
2387 pickup_lib - destination for hold, fallback value for selection_ou
2388 selection_ou - ID of org_unit establishing hard and soft hold boundary settings
2389 issuanceid - ID of the issuance to be held, required for Issuance level hold
2390 partid - ID of the monograph part to be held, required for monograph part level hold
2391 titleid - ID (BRN) of the title to be held, required for Title level hold
2392 volume_id - required for Volume level hold
2393 copy_id - required for Copy level hold
2394 mrid - required for Meta-record level hold
2395 hold_type - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record (default "T")
2397 All key/value pairs are passed on to do_possibility_checks.
2401 # FIXME: better params checking. what other params are required, if any?
2402 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
2403 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
2404 # used in conditionals, where it may be undefined, causing a warning.
2405 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
2407 sub check_title_hold {
2408 my( $self, $client, $authtoken, $params ) = @_;
2409 my $e = new_editor(authtoken=>$authtoken);
2410 return $e->event unless $e->checkauth;
2412 my %params = %$params;
2413 my $depth = $params{depth} || 0;
2414 $params{depth} = $depth; #define $params{depth} if unset, since it gets used later
2415 my $selection_ou = $params{selection_ou} || $params{pickup_lib};
2416 my $oargs = $params{oargs} || {};
2418 if($oargs->{events}) {
2419 @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
2423 my $patron = $e->retrieve_actor_user($params{patronid})
2424 or return $e->event;
2426 if( $e->requestor->id ne $patron->id ) {
2427 return $e->event unless
2428 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
2431 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
2433 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
2434 or return $e->event;
2436 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
2437 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
2440 my $return_depth = $hard_boundary; # default depth to return on success
2441 if(defined $soft_boundary and $depth < $soft_boundary) {
2442 # work up the tree and as soon as we find a potential copy, use that depth
2443 # also, make sure we don't go past the hard boundary if it exists
2445 # our min boundary is the greater of user-specified boundary or hard boundary
2446 my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
2447 $hard_boundary : $depth;
2449 my $depth = $soft_boundary;
2450 while($depth >= $min_depth) {
2451 $logger->info("performing hold possibility check with soft boundary $depth");
2452 @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
2454 $return_depth = $depth;
2459 } elsif(defined $hard_boundary and $depth < $hard_boundary) {
2460 # there is no soft boundary, enforce the hard boundary if it exists
2461 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
2462 @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
2464 # no boundaries defined, fall back to user specifed boundary or no boundary
2465 $logger->info("performing hold possibility check with no boundary");
2466 @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
2469 my $place_unfillable = 0;
2470 $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
2475 "depth" => $return_depth,
2476 "local_avail" => $status[1]
2478 } elsif ($status[2]) {
2479 my $n = scalar @{$status[2]};
2480 return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2482 return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2488 sub do_possibility_checks {
2489 my($e, $patron, $request_lib, $depth, %params) = @_;
2491 my $issuanceid = $params{issuanceid} || "";
2492 my $partid = $params{partid} || "";
2493 my $titleid = $params{titleid} || "";
2494 my $volid = $params{volume_id};
2495 my $copyid = $params{copy_id};
2496 my $mrid = $params{mrid} || "";
2497 my $pickup_lib = $params{pickup_lib};
2498 my $hold_type = $params{hold_type} || 'T';
2499 my $selection_ou = $params{selection_ou} || $pickup_lib;
2500 my $holdable_formats = $params{holdable_formats};
2501 my $oargs = $params{oargs} || {};
2508 if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
2510 return $e->event unless $copy = $e->retrieve_asset_copy($copyid);
2511 return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
2512 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2514 return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
2515 return verify_copy_for_hold(
2516 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
2519 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2521 return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
2522 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2524 return _check_volume_hold_is_possible(
2525 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2528 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
2530 return _check_title_hold_is_possible(
2531 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
2534 } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
2536 return _check_issuance_hold_is_possible(
2537 $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2540 } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
2542 return _check_monopart_hold_is_possible(
2543 $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2546 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2548 # pasing undef as the depth to filtered_records causes the depth
2549 # of the selection_ou to be used, which is not what we want here.
2552 my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
2554 for my $rec (@$recs) {
2555 @status = _check_title_hold_is_possible(
2556 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
2562 # else { Unrecognized hold_type ! } # FIXME: return error? or 0?
2565 sub MR_filter_records {
2572 my $opac_visible = shift;
2574 my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
2575 return $U->storagereq(
2576 'open-ils.storage.metarecord.filtered_records.atomic',
2577 $m, $f, $org_at_depth, $opac_visible
2580 __PACKAGE__->register_method(
2581 method => 'MR_filter_records',
2582 api_name => 'open-ils.circ.holds.metarecord.filtered_records',
2587 sub create_ranged_org_filter {
2588 my($e, $selection_ou, $depth) = @_;
2590 # find the orgs from which this hold may be fulfilled,
2591 # based on the selection_ou and depth
2593 my $top_org = $e->search_actor_org_unit([
2594 {parent_ou => undef},
2595 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
2598 return () if $depth == $top_org->ou_type->depth;
2600 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
2601 %org_filter = (circ_lib => []);
2602 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
2604 $logger->info("hold org filter at depth $depth and selection_ou ".
2605 "$selection_ou created list of @{$org_filter{circ_lib}}");
2611 sub _check_title_hold_is_possible {
2612 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
2613 # $holdable_formats is now unused. We pre-filter the MR's records.
2615 my $e = new_editor();
2616 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2618 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2619 my $copies = $e->json_query(
2621 select => { acp => ['id', 'circ_lib'] },
2626 fkey => 'call_number',
2627 filter => { record => $titleid }
2631 filter => { holdable => 't', deleted => 'f' },
2634 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' },
2635 acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2639 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2640 '+acpm' => { target_copy => undef } # ignore part-linked copies
2645 $logger->info("title possible found ".scalar(@$copies)." potential copies");
2649 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2650 "payload" => {"fail_part" => "no_ultimate_items"}
2655 # -----------------------------------------------------------------------
2656 # sort the copies into buckets based on their circ_lib proximity to
2657 # the patron's home_ou.
2658 # -----------------------------------------------------------------------
2660 my $home_org = $patron->home_ou;
2661 my $req_org = $request_lib->id;
2663 $prox_cache{$home_org} =
2664 $e->search_actor_org_unit_proximity({from_org => $home_org})
2665 unless $prox_cache{$home_org};
2666 my $home_prox = $prox_cache{$home_org};
2667 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2670 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2671 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2673 my @keys = sort { $a <=> $b } keys %buckets;
2676 if( $home_org ne $req_org ) {
2677 # -----------------------------------------------------------------------
2678 # shove the copies close to the request_lib into the primary buckets
2679 # directly before the farthest away copies. That way, they are not
2680 # given priority, but they are checked before the farthest copies.
2681 # -----------------------------------------------------------------------
2682 $prox_cache{$req_org} =
2683 $e->search_actor_org_unit_proximity({from_org => $req_org})
2684 unless $prox_cache{$req_org};
2685 my $req_prox = $prox_cache{$req_org};
2688 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2689 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2691 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2692 my $new_key = $highest_key - 0.5; # right before the farthest prox
2693 my @keys2 = sort { $a <=> $b } keys %buckets2;
2694 for my $key (@keys2) {
2695 last if $key >= $highest_key;
2696 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2700 @keys = sort { $a <=> $b } keys %buckets;
2705 my $age_protect_only = 0;
2706 OUTER: for my $key (@keys) {
2707 my @cps = @{$buckets{$key}};
2709 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2711 for my $copyid (@cps) {
2713 next if $seen{$copyid};
2714 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2715 my $copy = $e->retrieve_asset_copy($copyid);
2716 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2718 unless($title) { # grab the title if we don't already have it
2719 my $vol = $e->retrieve_asset_call_number(
2720 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2721 $title = $vol->record;
2724 @status = verify_copy_for_hold(
2725 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2727 $age_protect_only ||= $status[3];
2728 last OUTER if $status[0];
2732 $status[3] = $age_protect_only;
2736 sub _check_issuance_hold_is_possible {
2737 my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2739 my $e = new_editor();
2740 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2742 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2743 my $copies = $e->json_query(
2745 select => { acp => ['id', 'circ_lib'] },
2751 filter => { issuance => $issuanceid }
2755 filter => { holdable => 't', deleted => 'f' },
2758 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
2762 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
2768 $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
2772 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
2773 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2778 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2779 "payload" => {"fail_part" => "no_ultimate_items"}
2787 # -----------------------------------------------------------------------
2788 # sort the copies into buckets based on their circ_lib proximity to
2789 # the patron's home_ou.
2790 # -----------------------------------------------------------------------
2792 my $home_org = $patron->home_ou;
2793 my $req_org = $request_lib->id;
2795 $prox_cache{$home_org} =
2796 $e->search_actor_org_unit_proximity({from_org => $home_org})
2797 unless $prox_cache{$home_org};
2798 my $home_prox = $prox_cache{$home_org};
2799 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2802 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2803 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2805 my @keys = sort { $a <=> $b } keys %buckets;
2808 if( $home_org ne $req_org ) {
2809 # -----------------------------------------------------------------------
2810 # shove the copies close to the request_lib into the primary buckets
2811 # directly before the farthest away copies. That way, they are not
2812 # given priority, but they are checked before the farthest copies.
2813 # -----------------------------------------------------------------------
2814 $prox_cache{$req_org} =
2815 $e->search_actor_org_unit_proximity({from_org => $req_org})
2816 unless $prox_cache{$req_org};
2817 my $req_prox = $prox_cache{$req_org};
2820 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2821 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2823 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2824 my $new_key = $highest_key - 0.5; # right before the farthest prox
2825 my @keys2 = sort { $a <=> $b } keys %buckets2;
2826 for my $key (@keys2) {
2827 last if $key >= $highest_key;
2828 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2832 @keys = sort { $a <=> $b } keys %buckets;
2837 my $age_protect_only = 0;
2838 OUTER: for my $key (@keys) {
2839 my @cps = @{$buckets{$key}};
2841 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2843 for my $copyid (@cps) {
2845 next if $seen{$copyid};
2846 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2847 my $copy = $e->retrieve_asset_copy($copyid);
2848 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2850 unless($title) { # grab the title if we don't already have it
2851 my $vol = $e->retrieve_asset_call_number(
2852 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2853 $title = $vol->record;
2856 @status = verify_copy_for_hold(
2857 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2859 $age_protect_only ||= $status[3];
2860 last OUTER if $status[0];
2865 if (!defined($empty_ok)) {
2866 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
2867 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2870 return (1,0) if ($empty_ok);
2872 $status[3] = $age_protect_only;
2876 sub _check_monopart_hold_is_possible {
2877 my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2879 my $e = new_editor();
2880 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2882 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2883 my $copies = $e->json_query(
2885 select => { acp => ['id', 'circ_lib'] },
2889 field => 'target_copy',
2891 filter => { part => $partid }
2895 filter => { holdable => 't', deleted => 'f' },
2898 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
2902 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
2908 $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
2912 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
2913 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2918 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2919 "payload" => {"fail_part" => "no_ultimate_items"}
2927 # -----------------------------------------------------------------------
2928 # sort the copies into buckets based on their circ_lib proximity to
2929 # the patron's home_ou.
2930 # -----------------------------------------------------------------------
2932 my $home_org = $patron->home_ou;
2933 my $req_org = $request_lib->id;
2935 $prox_cache{$home_org} =
2936 $e->search_actor_org_unit_proximity({from_org => $home_org})
2937 unless $prox_cache{$home_org};
2938 my $home_prox = $prox_cache{$home_org};
2939 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2942 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2943 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2945 my @keys = sort { $a <=> $b } keys %buckets;
2948 if( $home_org ne $req_org ) {
2949 # -----------------------------------------------------------------------
2950 # shove the copies close to the request_lib into the primary buckets
2951 # directly before the farthest away copies. That way, they are not
2952 # given priority, but they are checked before the farthest copies.
2953 # -----------------------------------------------------------------------
2954 $prox_cache{$req_org} =
2955 $e->search_actor_org_unit_proximity({from_org => $req_org})
2956 unless $prox_cache{$req_org};
2957 my $req_prox = $prox_cache{$req_org};
2960 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2961 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2963 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2964 my $new_key = $highest_key - 0.5; # right before the farthest prox
2965 my @keys2 = sort { $a <=> $b } keys %buckets2;
2966 for my $key (@keys2) {
2967 last if $key >= $highest_key;
2968 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2972 @keys = sort { $a <=> $b } keys %buckets;
2977 my $age_protect_only = 0;
2978 OUTER: for my $key (@keys) {
2979 my @cps = @{$buckets{$key}};
2981 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2983 for my $copyid (@cps) {
2985 next if $seen{$copyid};
2986 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2987 my $copy = $e->retrieve_asset_copy($copyid);
2988 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2990 unless($title) { # grab the title if we don't already have it
2991 my $vol = $e->retrieve_asset_call_number(
2992 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2993 $title = $vol->record;
2996 @status = verify_copy_for_hold(
2997 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2999 $age_protect_only ||= $status[3];
3000 last OUTER if $status[0];
3005 if (!defined($empty_ok)) {
3006 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
3007 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
3010 return (1,0) if ($empty_ok);
3012 $status[3] = $age_protect_only;
3017 sub _check_volume_hold_is_possible {
3018 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
3019 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
3020 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
3021 $logger->info("checking possibility of volume hold for volume ".$vol->id);
3023 my $filter_copies = [];
3024 for my $copy (@$copies) {
3025 # ignore part-mapped copies for regular volume level holds
3026 push(@$filter_copies, $copy) unless
3027 new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
3029 $copies = $filter_copies;
3034 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3035 "payload" => {"fail_part" => "no_ultimate_items"}
3041 my $age_protect_only = 0;
3042 for my $copy ( @$copies ) {
3043 @status = verify_copy_for_hold(
3044 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
3045 $age_protect_only ||= $status[3];
3048 $status[3] = $age_protect_only;
3054 sub verify_copy_for_hold {
3055 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
3056 # $oargs should be undef unless we're overriding.
3057 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
3058 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3061 requestor => $requestor,
3064 title_descriptor => $title->fixed_fields,
3065 pickup_lib => $pickup_lib,
3066 request_lib => $request_lib,
3068 show_event_list => 1
3072 # Check for override permissions on events.
3073 if ($oargs && $permitted && scalar @$permitted) {
3074 # Remove the events from permitted that we can override.
3075 if ($oargs->{events}) {
3076 foreach my $evt (@{$oargs->{events}}) {
3077 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3080 # Now, we handle the override all case by checking remaining
3081 # events against override permissions.
3082 if (scalar @$permitted && $oargs->{all}) {
3083 # Pre-set events and failed members of oargs to empty
3084 # arrays, if they are not set, yet.
3085 $oargs->{events} = [] unless ($oargs->{events});
3086 $oargs->{failed} = [] unless ($oargs->{failed});
3087 # When we're done with these checks, we swap permitted
3088 # with a reference to @disallowed.
3089 my @disallowed = ();
3090 foreach my $evt (@{$permitted}) {
3091 # Check if we've already seen the event in this
3092 # session and it failed.
3093 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3094 push(@disallowed, $evt);
3096 # We have to check if the requestor has the
3097 # override permission.
3099 # AppUtils::check_user_perms returns the perm if
3100 # the user doesn't have it, undef if they do.
3101 if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3102 push(@disallowed, $evt);
3103 push(@{$oargs->{failed}}, $evt->{textcode});
3105 push(@{$oargs->{events}}, $evt->{textcode});
3109 $permitted = \@disallowed;
3113 my $age_protect_only = 0;
3114 if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3115 $age_protect_only = 1;
3119 (not scalar @$permitted), # true if permitted is an empty arrayref
3120 ( # XXX This test is of very dubious value; someone should figure
3121 # out what if anything is checking this value
3122 ($copy->circ_lib == $pickup_lib) and
3123 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3132 sub find_nearest_permitted_hold {
3135 my $editor = shift; # CStoreEditor object
3136 my $copy = shift; # copy to target
3137 my $user = shift; # staff
3138 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3140 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3142 my $bc = $copy->barcode;
3144 # find any existing holds that already target this copy
3145 my $old_holds = $editor->search_action_hold_request(
3146 { current_copy => $copy->id,
3147 cancel_time => undef,
3148 capture_time => undef
3152 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3154 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3155 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3157 my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3159 # the nearest_hold API call now needs this
3160 $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3161 unless ref $copy->call_number;
3163 # search for what should be the best holds for this copy to fulfill
3164 my $best_holds = $U->storagereq(
3165 "open-ils.storage.action.hold_request.nearest_hold.atomic",
3166 $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3168 # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3170 for my $holdid (@$old_holds) {
3171 next unless $holdid;
3172 push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3176 unless(@$best_holds) {
3177 $logger->info("circulator: no suitable holds found for copy $bc");
3178 return (undef, $evt);
3184 # for each potential hold, we have to run the permit script
3185 # to make sure the hold is actually permitted.
3188 for my $holdid (@$best_holds) {
3189 next unless $holdid;
3190 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3192 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3193 # Force and recall holds bypass all rules
3194 if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3198 my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3199 my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3201 $reqr_cache{$hold->requestor} = $reqr;
3202 $org_cache{$hold->request_lib} = $rlib;
3204 # see if this hold is permitted
3205 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3207 patron_id => $hold->usr,
3210 pickup_lib => $hold->pickup_lib,
3211 request_lib => $rlib,
3223 unless( $best_hold ) { # no "good" permitted holds were found
3225 $logger->info("circulator: no suitable holds found for copy $bc");
3226 return (undef, $evt);
3229 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3231 # indicate a permitted hold was found
3232 return $best_hold if $check_only;
3234 # we've found a permitted hold. we need to "grab" the copy
3235 # to prevent re-targeted holds (next part) from re-grabbing the copy
3236 $best_hold->current_copy($copy->id);
3237 $editor->update_action_hold_request($best_hold)
3238 or return (undef, $editor->event);
3243 # re-target any other holds that already target this copy
3244 for my $old_hold (@$old_holds) {
3245 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3246 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3247 $old_hold->id." after a better hold [".$best_hold->id."] was found");
3248 $old_hold->clear_current_copy;
3249 $old_hold->clear_prev_check_time;
3250 $editor->update_action_hold_request($old_hold)
3251 or return (undef, $editor->event);
3252 push(@retarget, $old_hold->id);
3255 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3263 __PACKAGE__->register_method(
3264 method => 'all_rec_holds',
3265 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3269 my( $self, $conn, $auth, $title_id, $args ) = @_;
3271 my $e = new_editor(authtoken=>$auth);
3272 $e->checkauth or return $e->event;
3273 $e->allowed('VIEW_HOLD') or return $e->event;
3276 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
3277 $args->{cancel_time} = undef;
3280 metarecord_holds => []
3282 , volume_holds => []
3284 , recall_holds => []
3287 , issuance_holds => []
3290 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3292 $resp->{metarecord_holds} = $e->search_action_hold_request(
3293 { hold_type => OILS_HOLD_TYPE_METARECORD,
3294 target => $mr_map->metarecord,
3300 $resp->{title_holds} = $e->search_action_hold_request(
3302 hold_type => OILS_HOLD_TYPE_TITLE,
3303 target => $title_id,
3307 my $parts = $e->search_biblio_monograph_part(
3313 $resp->{part_holds} = $e->search_action_hold_request(
3315 hold_type => OILS_HOLD_TYPE_MONOPART,
3321 my $subs = $e->search_serial_subscription(
3322 { record_entry => $title_id }, {idlist=>1});
3325 my $issuances = $e->search_serial_issuance(
3326 {subscription => $subs}, {idlist=>1}
3330 $resp->{issuance_holds} = $e->search_action_hold_request(
3332 hold_type => OILS_HOLD_TYPE_ISSUANCE,
3333 target => $issuances,
3340 my $vols = $e->search_asset_call_number(
3341 { record => $title_id, deleted => 'f' }, {idlist=>1});
3343 return $resp unless @$vols;
3345 $resp->{volume_holds} = $e->search_action_hold_request(
3347 hold_type => OILS_HOLD_TYPE_VOLUME,
3352 my $copies = $e->search_asset_copy(
3353 { call_number => $vols, deleted => 'f' }, {idlist=>1});
3355 return $resp unless @$copies;
3357 $resp->{copy_holds} = $e->search_action_hold_request(
3359 hold_type => OILS_HOLD_TYPE_COPY,
3364 $resp->{recall_holds} = $e->search_action_hold_request(
3366 hold_type => OILS_HOLD_TYPE_RECALL,
3371 $resp->{force_holds} = $e->search_action_hold_request(
3373 hold_type => OILS_HOLD_TYPE_FORCE,
3381 __PACKAGE__->register_method(
3382 method => 'stream_wide_holds',
3385 api_name => 'open-ils.circ.hold.wide_hash.stream'
3388 sub stream_wide_holds {
3389 my($self, $client, $auth, $restrictions, $order_by, $limit, $offset) = @_;
3391 my $e = new_editor(authtoken=>$auth);
3392 $e->checkauth or return $e->event;
3393 $e->allowed('VIEW_HOLD') or return $e->event;
3395 my $st = OpenSRF::AppSession->create('open-ils.storage');
3396 my $req = $st->request(
3397 'open-ils.storage.action.live_holds.wide_hash',
3398 $restrictions, $order_by, $limit, $offset
3401 my $count = $req->recv;
3406 if(UNIVERSAL::isa($count,"Error")) {
3407 throw $count ($count->stringify);
3410 $count = $count->content;
3412 # Force immediate send of count response
3413 my $mbc = $client->max_bundle_count;
3414 $client->max_bundle_count(1);
3415 $client->respond($count);
3416 $client->max_bundle_count($mbc);
3418 while (my $hold = $req->recv) {
3419 $client->respond($hold->content) if $hold->content;
3422 $client->respond_complete;
3428 __PACKAGE__->register_method(
3429 method => 'uber_hold',
3431 api_name => 'open-ils.circ.hold.details.retrieve'
3435 my($self, $client, $auth, $hold_id, $args) = @_;
3436 my $e = new_editor(authtoken=>$auth);
3437 $e->checkauth or return $e->event;
3438 return uber_hold_impl($e, $hold_id, $args);
3441 __PACKAGE__->register_method(
3442 method => 'batch_uber_hold',
3445 api_name => 'open-ils.circ.hold.details.batch.retrieve'
3448 sub batch_uber_hold {
3449 my($self, $client, $auth, $hold_ids, $args) = @_;
3450 my $e = new_editor(authtoken=>$auth);
3451 $e->checkauth or return $e->event;
3452 $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3456 sub uber_hold_impl {
3457 my($e, $hold_id, $args) = @_;
3460 my $flesh_fields = ['current_copy', 'usr', 'notes'];
3461 push (@$flesh_fields, 'requestor') if $args->{include_requestor};
3462 push (@$flesh_fields, 'cancel_cause') if $args->{include_cancel_cause};
3463 push (@$flesh_fields, 'sms_carrier') if $args->{include_sms_carrier};
3465 my $hold = $e->retrieve_action_hold_request([
3467 {flesh => 1, flesh_fields => {ahr => $flesh_fields}}
3468 ]) or return $e->event;
3470 if($hold->usr->id ne $e->requestor->id) {
3471 # caller is asking for someone else's hold
3472 $e->allowed('VIEW_HOLD') or return $e->event;
3473 $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3474 [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3477 # caller is asking for own hold, but may not have permission to view staff notes
3478 unless($e->allowed('VIEW_HOLD')) {
3479 $hold->notes( # filter out any staff notes (unless marked as public)
3480 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3484 my $user = $hold->usr;
3485 $hold->usr($user->id);
3488 my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args);
3490 flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3491 flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3493 my $details = retrieve_hold_queue_status_impl($e, $hold);
3494 $hold->usr($user) if $args->{include_usr}; # re-flesh
3499 ($copy ? (copy => $copy) : ()),
3500 ($volume ? (volume => $volume) : ()),
3501 ($issuance ? (issuance => $issuance) : ()),
3502 ($part ? (part => $part) : ()),
3503 ($args->{include_bre} ? (bre => $bre) : ()),
3504 ($args->{suppress_mvr} ? () : (mvr => $mvr)),
3508 $resp->{copy}->location(
3509 $e->retrieve_asset_copy_location($resp->{copy}->location))
3510 if $resp->{copy} and $args->{flesh_acpl};
3512 unless($args->{suppress_patron_details}) {
3513 my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3514 $resp->{patron_first} = $user->first_given_name,
3515 $resp->{patron_last} = $user->family_name,
3516 $resp->{patron_barcode} = $card->barcode,
3517 $resp->{patron_alias} = $user->alias,
3525 # -----------------------------------------------------
3526 # Returns the MVR object that represents what the
3528 # -----------------------------------------------------
3530 my( $e, $hold, $args ) = @_;
3538 my $no_mvr = $args->{suppress_mvr};
3540 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3541 $metarecord = $e->retrieve_metabib_metarecord($hold->target)
3542 or return $e->event;
3543 $tid = $metarecord->master_record;
3545 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3546 $tid = $hold->target;
3548 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3549 $volume = $e->retrieve_asset_call_number($hold->target)
3550 or return $e->event;
3551 $tid = $volume->record;
3553 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3554 $issuance = $e->retrieve_serial_issuance([
3556 {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3557 ]) or return $e->event;
3559 $tid = $issuance->subscription->record_entry;
3561 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3562 $part = $e->retrieve_biblio_monograph_part([
3564 ]) or return $e->event;
3566 $tid = $part->record;
3568 } 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 ) {
3569 $copy = $e->retrieve_asset_copy([
3571 {flesh => 1, flesh_fields => {acp => ['call_number']}}
3572 ]) or return $e->event;
3574 $volume = $copy->call_number;
3575 $tid = $volume->record;
3578 if(!$copy and ref $hold->current_copy ) {
3579 $copy = $hold->current_copy;
3580 $hold->current_copy($copy->id) unless $args->{include_current_copy};
3583 if(!$volume and $copy) {
3584 $volume = $e->retrieve_asset_call_number($copy->call_number);
3587 # TODO return metarcord mvr for M holds
3588 my $title = $e->retrieve_biblio_record_entry($tid);
3589 return ( ($no_mvr) ? undef : $U->record_to_mvr($title),
3590 $volume, $copy, $issuance, $part, $title, $metarecord);
3593 __PACKAGE__->register_method(
3594 method => 'clear_shelf_cache',
3595 api_name => 'open-ils.circ.hold.clear_shelf.get_cache',
3599 Returns the holds processed with the given cache key
3604 sub clear_shelf_cache {
3605 my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3606 my $e = new_editor(authtoken => $auth, xact => 1);
3607 return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3610 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3612 my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3615 $logger->info("no hold data found in cache"); # XXX TODO return event
3621 foreach (keys %$hold_data) {
3622 $maximum += scalar(@{ $hold_data->{$_} });
3624 $client->respond({"maximum" => $maximum, "progress" => 0});
3626 for my $action (sort keys %$hold_data) {
3627 while (@{$hold_data->{$action}}) {
3628 my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3630 my $result_chunk = $e->json_query({
3632 "acp" => ["barcode"],
3634 first_given_name second_given_name family_name alias
3637 "acnp" => [{column => "label", alias => "prefix"}],
3638 "acns" => [{column => "label", alias => "suffix"}],
3646 "field" => "id", "fkey" => "current_copy",
3649 "field" => "id", "fkey" => "call_number",
3652 "field" => "id", "fkey" => "record"
3655 "field" => "id", "fkey" => "prefix"
3658 "field" => "id", "fkey" => "suffix"
3662 "acpl" => {"field" => "id", "fkey" => "location"}
3665 "au" => {"field" => "id", "fkey" => "usr"}
3668 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3669 }, {"substream" => 1}) or return $e->die_event;
3673 +{"action" => $action, "hold_details" => $_}
3684 __PACKAGE__->register_method(
3685 method => 'clear_shelf_process',
3687 api_name => 'open-ils.circ.hold.clear_shelf.process',
3690 1. Find all holds that have expired on the holds shelf
3692 3. If a clear-shelf status is configured, put targeted copies into this status
3693 4. Divide copies into 3 groups: items to transit, items to reshelve, and items
3694 that are needed for holds. No subsequent action is taken on the holds
3695 or items after grouping.
3700 sub clear_shelf_process {
3701 my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
3703 my $e = new_editor(authtoken=>$auth);
3704 $e->checkauth or return $e->die_event;
3705 my $cache = OpenSRF::Utils::Cache->new('global');
3707 $org_id ||= $e->requestor->ws_ou;
3708 $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
3710 my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
3712 my @hold_ids = $self->method_lookup(
3713 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
3714 )->run($auth, $org_id, $match_copy);
3719 my @canceled_holds; # newly canceled holds
3720 $chunk_size ||= 25; # chunked status updates
3721 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3724 for my $hold_id (@hold_ids) {
3726 $logger->info("Clear shelf processing hold $hold_id");
3728 my $hold = $e->retrieve_action_hold_request([
3731 flesh_fields => {ahr => ['current_copy']}
3735 if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
3736 $hold->cancel_time('now');
3737 $hold->cancel_cause(2); # Hold Shelf expiration
3738 $e->update_action_hold_request($hold) or return $e->die_event;
3739 push(@canceled_holds, $hold_id);
3742 my $copy = $hold->current_copy;
3744 if($copy_status or $copy_status == 0) {
3745 # if a clear-shelf copy status is defined, update the copy
3746 $copy->status($copy_status);
3747 $copy->edit_date('now');
3748 $copy->editor($e->requestor->id);
3749 $e->update_asset_copy($copy) or return $e->die_event;
3752 push(@holds, $hold);
3753 $client->respond({maximum => scalar(@holds), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
3762 pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
3765 for my $hold (@holds) {
3767 my $copy = $hold->current_copy;
3768 my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
3770 if($alt_hold and !$match_copy) {
3772 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
3774 } elsif($copy->circ_lib != $e->requestor->ws_ou) {
3776 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
3780 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
3784 my $cache_key = md5_hex(time . $$ . rand());
3785 $logger->info("clear_shelf_cache: storing under $cache_key");
3786 $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours. configurable?
3788 # tell the client we're done
3789 $client->respond_complete({cache_key => $cache_key});
3792 # fire off the hold cancelation trigger and wait for response so don't flood the service
3794 # refetch the holds to pick up the caclulated cancel_time,
3795 # which may be needed by Action/Trigger
3797 my $updated_holds = [];
3798 $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
3801 $U->create_events_for_hook(
3802 'hold_request.cancel.expire_holds_shelf',
3803 $_, $org_id, undef, undef, 1) for @$updated_holds;
3806 # tell the client we're done
3807 $client->respond_complete;
3811 # returns IDs for holds that are on the holds shelf but
3812 # have had their pickup_libs change while on the shelf.
3813 sub pickup_lib_changed_on_shelf_holds {
3816 my $ignore_holds = shift;
3817 $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
3820 select => { alhr => ['id'] },
3825 fkey => 'current_copy'
3830 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
3832 capture_time => { "!=" => undef },
3833 fulfillment_time => undef,
3834 current_shelf_lib => $org_id,
3835 pickup_lib => {'!=' => {'+alhr' => 'current_shelf_lib'}}
3840 $query->{where}->{'+alhr'}->{id} =
3841 {'not in' => $ignore_holds} if @$ignore_holds;
3843 my $hold_ids = $e->json_query($query);
3844 return [ map { $_->{id} } @$hold_ids ];
3847 __PACKAGE__->register_method(
3848 method => 'usr_hold_summary',
3849 api_name => 'open-ils.circ.holds.user_summary',
3851 Returns a summary of holds statuses for a given user
3855 sub usr_hold_summary {
3856 my($self, $conn, $auth, $user_id) = @_;
3858 my $e = new_editor(authtoken=>$auth);
3859 $e->checkauth or return $e->event;
3860 $e->allowed('VIEW_HOLD') or return $e->event;
3862 my $holds = $e->search_action_hold_request(
3865 fulfillment_time => undef,
3866 cancel_time => undef,
3870 my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
3871 $summary{_hold_status($e, $_)} += 1 for @$holds;
3877 __PACKAGE__->register_method(
3878 method => 'hold_has_copy_at',
3879 api_name => 'open-ils.circ.hold.has_copy_at',
3882 'Returns the ID of the found copy and name of the shelving location if there is ' .
3883 'an available copy at the specified org unit. Returns empty hash otherwise. ' .
3884 'The anticipated use for this method is to determine whether an item is ' .
3885 'available at the library where the user is placing the hold (or, alternatively, '.
3886 'at the pickup library) to encourage bypassing the hold placement and just ' .
3887 'checking out the item.' ,
3889 { desc => 'Authentication Token', type => 'string' },
3890 { desc => 'Method Arguments. Options include: hold_type, hold_target, org_unit. '
3891 . 'hold_type is the hold type code (T, V, C, M, ...). '
3892 . 'hold_target is the identifier of the hold target object. '
3893 . 'org_unit is org unit ID.',
3898 desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
3904 sub hold_has_copy_at {
3905 my($self, $conn, $auth, $args) = @_;
3907 my $e = new_editor(authtoken=>$auth);
3908 $e->checkauth or return $e->event;
3910 my $hold_type = $$args{hold_type};
3911 my $hold_target = $$args{hold_target};
3912 my $org_unit = $$args{org_unit};
3915 select => {acp => ['id'], acpl => ['name']},
3920 filter => { holdable => 't', deleted => 'f' },
3923 ccs => {field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3926 where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit, status => [0,7]}},
3930 if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
3932 $query->{where}->{'+acp'}->{id} = $hold_target;
3934 } elsif($hold_type eq 'V') {
3936 $query->{where}->{'+acp'}->{call_number} = $hold_target;
3938 } elsif($hold_type eq 'P') {
3940 $query->{from}->{acp}->{acpm} = {
3941 field => 'target_copy',
3943 filter => {part => $hold_target},
3946 } elsif($hold_type eq 'I') {
3948 $query->{from}->{acp}->{sitem} = {
3951 filter => {issuance => $hold_target},
3954 } elsif($hold_type eq 'T') {
3956 $query->{from}->{acp}->{acn} = {
3958 fkey => 'call_number',
3962 filter => {id => $hold_target},
3970 $query->{from}->{acp}->{acn} = {
3972 fkey => 'call_number',
3981 filter => {metarecord => $hold_target},
3989 my $res = $e->json_query($query)->[0] or return {};
3990 return {copy => $res->{id}, location => $res->{name}} if $res;
3994 # returns true if the user already has an item checked out
3995 # that could be used to fulfill the requested hold.
3996 sub hold_item_is_checked_out {
3997 my($e, $user_id, $hold_type, $hold_target) = @_;
4000 select => {acp => ['id']},
4001 from => {acp => {}},
4005 in => { # copies for circs the user has checked out
4006 select => {circ => ['target_copy']},
4010 checkin_time => undef,
4012 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
4013 {stop_fines => undef}
4023 if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
4025 $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
4027 } elsif($hold_type eq 'V') {
4029 $query->{where}->{'+acp'}->{call_number} = $hold_target;
4031 } elsif($hold_type eq 'P') {
4033 $query->{from}->{acp}->{acpm} = {
4034 field => 'target_copy',
4036 filter => {part => $hold_target},
4039 } elsif($hold_type eq 'I') {
4041 $query->{from}->{acp}->{sitem} = {
4044 filter => {issuance => $hold_target},
4047 } elsif($hold_type eq 'T') {
4049 $query->{from}->{acp}->{acn} = {
4051 fkey => 'call_number',
4055 filter => {id => $hold_target},
4063 $query->{from}->{acp}->{acn} = {
4065 fkey => 'call_number',
4074 filter => {metarecord => $hold_target},
4082 return $e->json_query($query)->[0];
4085 __PACKAGE__->register_method(
4086 method => 'change_hold_title',
4087 api_name => 'open-ils.circ.hold.change_title',
4090 Updates all title level holds targeting the specified bibs to point a new bib./,
4092 { desc => 'Authentication Token', type => 'string' },
4093 { desc => 'New Target Bib Id', type => 'number' },
4094 { desc => 'Old Target Bib Ids', type => 'array' },
4096 return => { desc => '1 on success' }
4100 __PACKAGE__->register_method(
4101 method => 'change_hold_title_for_specific_holds',
4102 api_name => 'open-ils.circ.hold.change_title.specific_holds',
4105 Updates specified holds to target new bib./,
4107 { desc => 'Authentication Token', type => 'string' },
4108 { desc => 'New Target Bib Id', type => 'number' },
4109 { desc => 'Holds Ids for holds to update', type => 'array' },
4111 return => { desc => '1 on success' }
4116 sub change_hold_title {
4117 my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4119 my $e = new_editor(authtoken=>$auth, xact=>1);
4120 return $e->die_event unless $e->checkauth;
4122 my $holds = $e->search_action_hold_request(
4125 capture_time => undef,
4126 cancel_time => undef,
4127 fulfillment_time => undef,
4133 flesh_fields => { ahr => ['usr'] }
4139 for my $hold (@$holds) {
4140 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4141 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4142 $hold->target( $new_bib_id );
4143 $e->update_action_hold_request($hold) or return $e->die_event;
4148 _reset_hold($self, $e->requestor, $_) for @$holds;
4153 sub change_hold_title_for_specific_holds {
4154 my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4156 my $e = new_editor(authtoken=>$auth, xact=>1);
4157 return $e->die_event unless $e->checkauth;
4159 my $holds = $e->search_action_hold_request(
4162 capture_time => undef,
4163 cancel_time => undef,
4164 fulfillment_time => undef,
4170 flesh_fields => { ahr => ['usr'] }
4176 for my $hold (@$holds) {
4177 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4178 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4179 $hold->target( $new_bib_id );
4180 $e->update_action_hold_request($hold) or return $e->die_event;
4185 _reset_hold($self, $e->requestor, $_) for @$holds;
4190 __PACKAGE__->register_method(
4191 method => 'rec_hold_count',
4192 api_name => 'open-ils.circ.bre.holds.count',
4194 desc => q/Returns the total number of holds that target the
4195 selected bib record or its associated copies and call_numbers/,
4197 { desc => 'Bib ID', type => 'number' },
4198 { desc => q/Optional arguments. Supported arguments include:
4199 "pickup_lib_descendant" -> limit holds to those whose pickup
4200 library is equal to or is a child of the provided org unit/,
4204 return => {desc => 'Hold count', type => 'number'}
4208 __PACKAGE__->register_method(
4209 method => 'rec_hold_count',
4210 api_name => 'open-ils.circ.mmr.holds.count',
4212 desc => q/Returns the total number of holds that target the
4213 selected metarecord or its associated copies, call_numbers, and bib records/,
4215 { desc => 'Metarecord ID', type => 'number' },
4217 return => {desc => 'Hold count', type => 'number'}
4221 # XXX Need to add type I holds to these counts
4222 sub rec_hold_count {
4223 my($self, $conn, $target_id, $args) = @_;
4230 filter => {metarecord => $target_id}
4237 filter => { id => $target_id },
4242 if($self->api_name =~ /mmr/) {
4243 delete $bre_join->{bre}->{filter};
4244 $bre_join->{bre}->{join} = $mmr_join;
4250 fkey => 'call_number',
4256 select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4260 cancel_time => undef,
4261 fulfillment_time => undef,
4265 hold_type => [qw/C F R/],
4268 select => {acp => ['id']},
4269 from => { acp => $cn_join }
4279 select => {acn => ['id']},
4280 from => {acn => $bre_join}
4290 select => {bmp => ['id']},
4291 from => {bmp => $bre_join}
4299 target => $target_id
4307 if($self->api_name =~ /mmr/) {
4308 $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4313 select => {bre => ['id']},
4314 from => {bre => $mmr_join}
4320 $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4323 target => $target_id
4329 if (my $pld = $args->{pickup_lib_descendant}) {
4331 my $top_ou = new_editor()->search_actor_org_unit(
4332 {parent_ou => undef}
4333 )->[0]; # XXX Assumes single root node. Not alone in this...
4335 $query->{where}->{'+ahr'}->{pickup_lib} = {
4337 select => {aou => [{
4339 transform => 'actor.org_unit_descendants',
4340 result_field => 'id'
4343 where => {id => $pld}
4345 } if ($pld != $top_ou->id);
4348 # To avoid Internal Server Errors, we get an editor, then run the
4349 # query and check the result. If anything fails, we'll return 0.
4351 if (my $e = new_editor()) {
4352 my $query_result = $e->json_query($query);
4353 if ($query_result && @{$query_result}) {
4354 $result = $query_result->[0]->{count}
4361 # A helper function to calculate a hold's expiration time at a given
4362 # org_unit. Takes the org_unit as an argument and returns either the
4363 # hold expire time as an ISO8601 string or undef if there is no hold
4364 # expiration interval set for the subject ou.
4365 sub calculate_expire_time
4368 my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4370 my $date = DateTime->now->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($interval));
4371 return $U->epoch2ISO8601($date->epoch);
4377 __PACKAGE__->register_method(
4378 method => 'mr_hold_filter_attrs',
4379 api_name => 'open-ils.circ.mmr.holds.filters',
4384 Returns the set of available formats and languages for the
4385 constituent records of the provided metarcord.
4386 If an array of hold IDs is also provided, information about
4387 each is returned as well. This information includes:
4388 1. a slightly easier to read version of holdable_formats
4389 2. attributes describing the set of format icons included
4390 in the set of desired, constituent records.
4393 {desc => 'Metarecord ID', type => 'number'},
4394 {desc => 'Context Org ID', type => 'number'},
4395 {desc => 'Hold ID List', type => 'array'},
4399 Stream of objects. The first will have a 'metarecord' key
4400 containing non-hold-specific metarecord information, subsequent
4401 responses will contain a 'hold' key containing hold-specific
4409 sub mr_hold_filter_attrs {
4410 my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4411 my $e = new_editor();
4413 # by default, return MR / hold attributes for all constituent
4414 # records with holdable copies. If there is a hard boundary,
4415 # though, limit to records with copies within the boundary,
4416 # since anything outside the boundary can never be held.
4419 $org_depth = $U->ou_ancestor_setting_value(
4420 $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4423 # get all org-scoped records w/ holdable copies for this metarecord
4424 my ($bre_ids) = $self->method_lookup(
4425 'open-ils.circ.holds.metarecord.filtered_records')->run(
4426 $mr_id, undef, $org_id, $org_depth);
4428 my $item_lang_attr = 'item_lang'; # configurable?
4429 my $format_attr = $e->retrieve_config_global_flag(
4430 'opac.metarecord.holds.format_attr')->value;
4432 # helper sub for fetching ccvms for a batch of record IDs
4433 sub get_batch_ccvms {
4434 my ($e, $attr, $bre_ids) = @_;
4435 return [] unless $bre_ids and @$bre_ids;
4436 my $vals = $e->search_metabib_record_attr_flat({
4440 return [] unless @$vals;
4441 return $e->search_config_coded_value_map({
4443 code => [map {$_->value} @$vals]
4447 my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4448 my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4453 formats => $formats,
4458 return unless $hold_ids;
4459 my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4460 $icon_attr = $icon_attr ? $icon_attr->value : '';
4462 for my $hold_id (@$hold_ids) {
4463 my $hold = $e->retrieve_action_hold_request($hold_id)
4464 or return $e->event;
4466 next unless $hold->hold_type eq 'M';
4476 # collect the ccvm's for the selected formats / language
4477 # (i.e. the holdable formats) on the MR.
4478 # this assumes a two-key structure for format / language,
4479 # though no assumption is made about the keys themselves.
4480 my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4482 my $format_vals = [];
4483 for my $val (values %$hformats) {
4484 # val is either a single ccvm or an array of them
4485 $val = [$val] unless ref $val eq 'ARRAY';
4486 for my $node (@$val) {
4487 push (@$lang_vals, $node->{_val})
4488 if $node->{_attr} eq $item_lang_attr;
4489 push (@$format_vals, $node->{_val})
4490 if $node->{_attr} eq $format_attr;
4494 # fetch the ccvm's for consistency with the {metarecord} blob
4495 $resp->{hold}{formats} = $e->search_config_coded_value_map({
4496 ctype => $format_attr, code => $format_vals});
4497 $resp->{hold}{langs} = $e->search_config_coded_value_map({
4498 ctype => $item_lang_attr, code => $lang_vals});
4500 # find all of the bib records within this metarcord whose
4501 # format / language match the holdable formats on the hold
4502 my ($bre_ids) = $self->method_lookup(
4503 'open-ils.circ.holds.metarecord.filtered_records')->run(
4504 $hold->target, $hold->holdable_formats,
4505 $hold->selection_ou, $hold->selection_depth);
4507 # now find all of the 'icon' attributes for the records
4508 $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4509 $client->respond($resp);
4515 __PACKAGE__->register_method(
4516 method => "copy_has_holds_count",
4517 api_name => "open-ils.circ.copy.has_holds_count",
4521 Returns the number of holds a paticular copy has
4524 { desc => 'Authentication Token', type => 'string'},
4525 { desc => 'Copy ID', type => 'number'}
4536 sub copy_has_holds_count {
4537 my( $self, $conn, $auth, $copyid ) = @_;
4538 my $e = new_editor(authtoken=>$auth);
4539 return $e->event unless $e->checkauth;
4541 if( $copyid && $copyid > 0 ) {
4542 my $meth = 'retrieve_action_has_holds_count';
4543 my $data = $e->$meth($copyid);
4545 return $data->count();
4551 __PACKAGE__->register_method(
4552 method => "hold_metadata",
4553 api_name => "open-ils.circ.hold.get_metadata",
4558 Returns a stream of objects containing whatever bib,
4559 volume, etc. data is available to the specific hold
4563 {desc => 'Hold Type', type => 'string'},
4564 {desc => 'Hold Target(s)', type => 'number or array'},
4565 {desc => 'Context org unit (optional)', type => 'number'}
4569 Stream of hold metadata objects.
4577 my ($self, $client, $hold_type, $hold_targets, $org_id) = @_;
4579 $hold_targets = [$hold_targets] unless ref $hold_targets;
4581 my $e = new_editor();
4582 for my $target (@$hold_targets) {
4584 # create a dummy hold for find_hold_mvr
4585 my $hold = Fieldmapper::action::hold_request->new;
4586 $hold->hold_type($hold_type);
4587 $hold->target($target);
4589 my (undef, $volume, $copy, $issuance, $part, $bre, $metarecord) =
4590 find_hold_mvr($e, $hold, {suppress_mvr => 1});
4592 $bre->clear_marc; # avoid bulk
4598 issuance => $issuance,
4601 metarecord => $metarecord,
4602 metarecord_filters => {}
4605 # If this is a bib hold or metarecord hold, also return the
4606 # available set of MR filters (AKA "Holdable Formats") for the
4607 # hold. For bib holds these may be used to upgrade the hold
4608 # from a bib to metarecord hold.
4609 if ($hold_type eq 'T') {
4610 my $map = $e->search_metabib_metarecord_source_map(
4611 {source => $meta->{bibrecord}->id})->[0];
4614 $meta->{metarecord} =
4615 $e->retrieve_metabib_metarecord($map->metarecord);
4619 if ($meta->{metarecord}) {
4622 $self->method_lookup('open-ils.circ.mmr.holds.filters')
4623 ->run($meta->{metarecord}->id, $org_id);
4626 $meta->{metarecord_filters} = $filters->{metarecord};
4630 $client->respond($meta);