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 # Remove oargs from params so holds can be created.
112 if ($$params{oargs}) {
113 delete $$params{oargs};
116 my $ahr = construct_hold_request_object($params);
117 my ($res2) = $self->method_lookup(
119 ? 'open-ils.circ.holds.create.override'
120 : 'open-ils.circ.holds.create'
121 )->run($auth, $ahr, $oargs);
123 'target' => $$params{$target_field},
126 $conn->respond($res2);
129 'target' => $$params{$target_field},
132 $conn->respond($res);
138 sub construct_hold_request_object {
141 my $ahr = Fieldmapper::action::hold_request->new;
144 foreach my $field (keys %{ $params }) {
145 if ($field eq 'depth') { $ahr->selection_depth($$params{$field}); }
146 elsif ($field eq 'patronid') {
147 $ahr->usr($$params{$field}); }
148 elsif ($field eq 'titleid') { $ahr->target($$params{$field}); }
149 elsif ($field eq 'copy_id') { $ahr->target($$params{$field}); }
150 elsif ($field eq 'issuanceid') { $ahr->target($$params{$field}); }
151 elsif ($field eq 'volume_id') { $ahr->target($$params{$field}); }
152 elsif ($field eq 'mrid') { $ahr->target($$params{$field}); }
153 elsif ($field eq 'partid') { $ahr->target($$params{$field}); }
155 $ahr->$field($$params{$field});
161 __PACKAGE__->register_method(
162 method => "create_hold_batch",
163 api_name => "open-ils.circ.holds.create.batch",
166 desc => q/@see open-ils.circ.holds.create.batch/,
168 { desc => 'Authentication token', type => 'string' },
169 { desc => 'Array of hold objects', type => 'array' }
172 desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
177 __PACKAGE__->register_method(
178 method => "create_hold_batch",
179 api_name => "open-ils.circ.holds.create.override.batch",
182 desc => '@see open-ils.circ.holds.create.batch',
187 sub create_hold_batch {
188 my( $self, $conn, $auth, $hold_list, $oargs ) = @_;
189 (my $method = $self->api_name) =~ s/\.batch//og;
190 foreach (@$hold_list) {
191 my ($res) = $self->method_lookup($method)->run($auth, $_, $oargs);
192 $conn->respond($res);
198 __PACKAGE__->register_method(
199 method => "create_hold",
200 api_name => "open-ils.circ.holds.create",
202 desc => "Create a new hold for an item. From a permissions perspective, " .
203 "the login session is used as the 'requestor' of the hold. " .
204 "The hold recipient is determined by the 'usr' setting within the hold object. " .
205 'First we verify the requestor has holds request permissions. ' .
206 'Then we verify that the recipient is allowed to make the given hold. ' .
207 'If not, we see if the requestor has "override" capabilities. If not, ' .
208 'a permission exception is returned. If permissions allow, we cycle ' .
209 'through the set of holds objects and create. ' .
210 'If the recipient does not have permission to place multiple holds ' .
211 'on a single title and said operation is attempted, a permission ' .
212 'exception is returned',
214 { desc => 'Authentication token', type => 'string' },
215 { desc => 'Hold object for hold to be created',
216 type => 'object', class => 'ahr' }
219 desc => 'New ahr ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
224 __PACKAGE__->register_method(
225 method => "create_hold",
226 api_name => "open-ils.circ.holds.create.override",
227 notes => '@see open-ils.circ.holds.create',
229 desc => "If the recipient is not allowed to receive the requested hold, " .
230 "call this method to attempt the override",
232 { desc => 'Authentication token', type => 'string' },
234 desc => 'Hold object for hold to be created',
235 type => 'object', class => 'ahr'
239 desc => 'New hold (ahr) ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
245 my( $self, $conn, $auth, $hold, $oargs ) = @_;
246 return -1 unless $hold;
247 my $e = new_editor(authtoken=>$auth, xact=>1);
248 return $e->die_event unless $e->checkauth;
251 if ($self->api_name =~ /override/) {
253 $oargs = { all => 1 } unless defined $oargs;
258 my $requestor = $e->requestor;
259 my $recipient = $requestor;
261 if( $requestor->id ne $hold->usr ) {
262 # Make sure the requestor is allowed to place holds for
263 # the recipient if they are not the same people
264 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->die_event;
265 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->die_event;
268 # If the related org setting tells us to, block if patron privs have expired
269 my $expire_setting = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_BLOCK_HOLD_FOR_EXPIRED_PATRON);
270 if ($expire_setting) {
271 my $expire = DateTime::Format::ISO8601->new->parse_datetime(
272 clean_ISO8601($recipient->expire_date));
274 push( @events, OpenILS::Event->new(
275 'PATRON_ACCOUNT_EXPIRED',
276 "payload" => {"fail_part" => "actor.usr.privs_expired"}
277 )) if( CORE::time > $expire->epoch ) ;
280 # Now make sure the recipient is allowed to receive the specified hold
281 my $porg = $recipient->home_ou;
282 my $rid = $e->requestor->id;
283 my $t = $hold->hold_type;
285 # See if a duplicate hold already exists
287 usr => $recipient->id,
289 fulfillment_time => undef,
290 target => $hold->target,
291 cancel_time => undef,
294 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
296 my $existing = $e->search_action_hold_request($sargs);
298 # See if the requestor has the CREATE_DUPLICATE_HOLDS perm.
299 my $can_dup = $e->allowed('CREATE_DUPLICATE_HOLDS', $recipient->home_ou);
300 # How many are allowed.
301 my $num_dups = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_MAX_DUPLICATE_HOLDS, $e) || 0;
302 push( @events, OpenILS::Event->new('HOLD_EXISTS'))
303 unless (($t eq 'T' || $t eq 'M') && $can_dup && scalar(@$existing) < $num_dups);
304 # Note: We check for @$existing < $num_dups because we're adding a hold with this call.
307 my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
308 push( @events, OpenILS::Event->new('HOLD_ITEM_CHECKED_OUT')) if $checked_out;
310 if ( $t eq OILS_HOLD_TYPE_METARECORD ) {
311 return $e->die_event unless $e->allowed('MR_HOLDS', $porg);
312 } elsif ( $t eq OILS_HOLD_TYPE_TITLE ) {
313 return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
314 } elsif ( $t eq OILS_HOLD_TYPE_VOLUME ) {
315 return $e->die_event unless $e->allowed('VOLUME_HOLDS', $porg);
316 } elsif ( $t eq OILS_HOLD_TYPE_MONOPART ) {
317 return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
318 } elsif ( $t eq OILS_HOLD_TYPE_ISSUANCE ) {
319 return $e->die_event unless $e->allowed('ISSUANCE_HOLDS', $porg);
320 } elsif ( $t eq OILS_HOLD_TYPE_COPY ) {
321 return $e->die_event unless $e->allowed('COPY_HOLDS', $porg);
322 } elsif ( $t eq OILS_HOLD_TYPE_FORCE || $t eq OILS_HOLD_TYPE_RECALL ) {
323 my $copy = $e->retrieve_asset_copy($hold->target)
324 or return $e->die_event;
325 if ( $t eq OILS_HOLD_TYPE_FORCE ) {
326 return $e->die_event unless $e->allowed('COPY_HOLDS_FORCE', $copy->circ_lib);
327 } elsif ( $t eq OILS_HOLD_TYPE_RECALL ) {
328 return $e->die_event unless $e->allowed('COPY_HOLDS_RECALL', $copy->circ_lib);
337 for my $evt (@events) {
339 my $name = $evt->{textcode};
340 if($oargs->{all} || grep { $_ eq $name } @{$oargs->{events}}) {
341 return $e->die_event unless $e->allowed("$name.override", $porg);
349 # Check for hold expiration in the past, and set it to empty string.
350 $hold->expire_time(undef) if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1);
352 # set the configured expire time
353 unless($hold->expire_time || $U->is_true($hold->frozen)) {
354 $hold->expire_time(calculate_expire_time($recipient->home_ou));
358 # if behind-the-desk pickup is supported at the hold pickup lib,
359 # set the value to the patron default, unless a value has already
360 # been applied. If it's not supported, force the value to false.
362 my $bdous = $U->ou_ancestor_setting_value(
364 'circ.holds.behind_desk_pickup_supported', $e);
367 if (!defined $hold->behind_desk) {
369 my $set = $e->search_actor_user_setting({
371 name => 'circ.holds_behind_desk'
374 $hold->behind_desk('t') if $set and
375 OpenSRF::Utils::JSON->JSON2perl($set->value);
378 # behind the desk not supported, force it to false
379 $hold->behind_desk('f');
382 $hold->requestor($e->requestor->id);
383 $hold->request_lib($e->requestor->ws_ou);
384 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
385 $hold = $e->create_action_hold_request($hold) or return $e->die_event;
389 $conn->respond_complete($hold->id);
391 $U->simplereq('open-ils.hold-targeter',
392 'open-ils.hold-targeter.target', {hold => $hold->id}
393 ) unless $U->is_true($hold->frozen);
398 # makes sure that a user has permission to place the type of requested hold
399 # returns the Perm exception if not allowed, returns undef if all is well
400 sub _check_holds_perm {
401 my($type, $user_id, $org_id) = @_;
405 $evt = $apputils->check_perms($user_id, $org_id, "MR_HOLDS" );
406 } elsif ($type eq "T") {
407 $evt = $apputils->check_perms($user_id, $org_id, "TITLE_HOLDS" );
408 } elsif($type eq "V") {
409 $evt = $apputils->check_perms($user_id, $org_id, "VOLUME_HOLDS");
410 } elsif($type eq "C") {
411 $evt = $apputils->check_perms($user_id, $org_id, "COPY_HOLDS" );
418 # tests if the given user is allowed to place holds on another's behalf
419 sub _check_request_holds_perm {
422 if (my $evt = $apputils->check_perms(
423 $user_id, $org_id, "REQUEST_HOLDS")) {
428 my $ses_is_req_note = 'The login session is the requestor. If the requestor is different from the user, ' .
429 'then the requestor must have VIEW_HOLD permissions';
431 __PACKAGE__->register_method(
432 method => "retrieve_holds_by_id",
433 api_name => "open-ils.circ.holds.retrieve_by_id",
435 desc => "Retrieve the hold, with hold transits attached, for the specified ID. $ses_is_req_note",
437 { desc => 'Authentication token', type => 'string' },
438 { desc => 'Hold ID', type => 'number' }
441 desc => 'Hold object with transits attached, event on error',
447 sub retrieve_holds_by_id {
448 my($self, $client, $auth, $hold_id) = @_;
449 my $e = new_editor(authtoken=>$auth);
450 $e->checkauth or return $e->event;
451 $e->allowed('VIEW_HOLD') or return $e->event;
453 my $holds = $e->search_action_hold_request(
455 { id => $hold_id , fulfillment_time => undef },
457 order_by => { ahr => "request_time" },
459 flesh_fields => {ahr => ['notes']}
464 flesh_hold_transits($holds);
465 flesh_hold_notices($holds, $e);
470 __PACKAGE__->register_method(
471 method => "retrieve_holds",
472 api_name => "open-ils.circ.holds.retrieve",
474 desc => "Retrieves all the holds, with hold transits attached, for the specified user. $ses_is_req_note",
476 { desc => 'Authentication token', type => 'string' },
477 { desc => 'User ID', type => 'integer' },
478 { desc => 'Available Only', type => 'boolean' }
481 desc => 'list of holds, event on error',
486 __PACKAGE__->register_method(
487 method => "retrieve_holds",
488 api_name => "open-ils.circ.holds.id_list.retrieve",
491 desc => "Retrieves all the hold IDs, for the specified user. $ses_is_req_note",
493 { desc => 'Authentication token', type => 'string' },
494 { desc => 'User ID', type => 'integer' },
495 { desc => 'Available Only', type => 'boolean' }
498 desc => 'list of holds, event on error',
503 __PACKAGE__->register_method(
504 method => "retrieve_holds",
505 api_name => "open-ils.circ.holds.canceled.retrieve",
508 desc => "Retrieves all the cancelled holds for the specified user. $ses_is_req_note",
510 { desc => 'Authentication token', type => 'string' },
511 { desc => 'User ID', type => 'integer' }
514 desc => 'list of holds, event on error',
519 __PACKAGE__->register_method(
520 method => "retrieve_holds",
521 api_name => "open-ils.circ.holds.canceled.id_list.retrieve",
524 desc => "Retrieves list of cancelled hold IDs for the specified user. $ses_is_req_note",
526 { desc => 'Authentication token', type => 'string' },
527 { desc => 'User ID', type => 'integer' }
530 desc => 'list of hold IDs, event on error',
537 my ($self, $client, $auth, $user_id, $available) = @_;
539 my $e = new_editor(authtoken=>$auth);
540 return $e->event unless $e->checkauth;
541 $user_id = $e->requestor->id unless defined $user_id;
543 my $notes_filter = {staff => 'f'};
544 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
545 unless($user_id == $e->requestor->id) {
546 if($e->allowed('VIEW_HOLD', $user->home_ou)) {
547 $notes_filter = {staff => 't'}
549 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
550 $e, $user_id, $e->requestor->id, 'hold.view');
551 return $e->event unless $allowed;
554 # staff member looking at his/her own holds can see staff and non-staff notes
555 $notes_filter = {} if $e->allowed('VIEW_HOLD', $user->home_ou);
559 select => {ahr => ['id']},
561 where => {usr => $user_id, fulfillment_time => undef}
564 if($self->api_name =~ /canceled/) {
566 # Fetch the canceled holds
567 # order cancelled holds by cancel time, most recent first
569 $holds_query->{order_by} = [{class => 'ahr', field => 'cancel_time', direction => 'desc'}];
572 my $cancel_count = $U->ou_ancestor_setting_value(
573 $e->requestor->ws_ou, 'circ.holds.canceled.display_count', $e);
575 unless($cancel_count) {
576 $cancel_age = $U->ou_ancestor_setting_value(
577 $e->requestor->ws_ou, 'circ.holds.canceled.display_age', $e);
579 # if no settings are defined, default to last 10 cancelled holds
580 $cancel_count = 10 unless $cancel_age;
583 if($cancel_count) { # limit by count
585 $holds_query->{where}->{cancel_time} = {'!=' => undef};
586 $holds_query->{limit} = $cancel_count;
588 } elsif($cancel_age) { # limit by age
590 # find all of the canceled holds that were canceled within the configured time frame
591 my $date = DateTime->now->subtract(seconds => OpenILS::Utils::DateTime->interval_to_seconds($cancel_age));
592 $date = $U->epoch2ISO8601($date->epoch);
593 $holds_query->{where}->{cancel_time} = {'>=' => $date};
598 # order non-cancelled holds by ready-for-pickup, then active, followed by suspended
599 # "compare" sorts false values to the front. testing pickup_lib != current_shelf_lib
600 # will sort by pl = csl > pl != csl > followed by csl is null;
601 $holds_query->{order_by} = [
603 field => 'pickup_lib',
604 compare => {'!=' => {'+ahr' => 'current_shelf_lib'}}},
605 {class => 'ahr', field => 'shelf_time'},
606 {class => 'ahr', field => 'frozen'},
607 {class => 'ahr', field => 'request_time'}
610 $holds_query->{where}->{cancel_time} = undef;
612 $holds_query->{where}->{shelf_time} = {'!=' => undef};
614 $holds_query->{where}->{pickup_lib} = {'=' => {'+ahr' => 'current_shelf_lib'}};
618 my $hold_ids = $e->json_query($holds_query);
619 $hold_ids = [ map { $_->{id} } @$hold_ids ];
621 return $hold_ids if $self->api_name =~ /id_list/;
624 for my $hold_id ( @$hold_ids ) {
626 my $hold = $e->retrieve_action_hold_request($hold_id);
627 $hold->notes($e->search_action_hold_request_note({hold => $hold_id, %$notes_filter}));
630 $e->search_action_hold_transit_copy([
631 {hold => $hold->id, cancel_time => undef},
632 {order_by => {ahtc => 'source_send_time desc'}, limit => 1}])->[0]
642 __PACKAGE__->register_method(
643 method => 'user_hold_count',
644 api_name => 'open-ils.circ.hold.user.count'
647 sub user_hold_count {
648 my ( $self, $conn, $auth, $userid ) = @_;
649 my $e = new_editor( authtoken => $auth );
650 return $e->event unless $e->checkauth;
651 my $patron = $e->retrieve_actor_user($userid)
653 return $e->event unless $e->allowed( 'VIEW_HOLD', $patron->home_ou );
654 return __user_hold_count( $self, $e, $userid );
657 sub __user_hold_count {
658 my ( $self, $e, $userid ) = @_;
659 my $holds = $e->search_action_hold_request(
662 fulfillment_time => undef,
663 cancel_time => undef,
668 return scalar(@$holds);
672 __PACKAGE__->register_method(
673 method => "retrieve_holds_by_pickup_lib",
674 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
676 "Retrieves all the holds, with hold transits attached, for the specified pickup_ou id."
679 __PACKAGE__->register_method(
680 method => "retrieve_holds_by_pickup_lib",
681 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
682 notes => "Retrieves all the hold ids for the specified pickup_ou id. "
685 sub retrieve_holds_by_pickup_lib {
686 my ($self, $client, $login_session, $ou_id) = @_;
688 #FIXME -- put an appropriate permission check here
689 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
690 # $login_session, $user_id, 'VIEW_HOLD' );
691 #return $evt if $evt;
693 my $holds = $apputils->simplereq(
695 "open-ils.cstore.direct.action.hold_request.search.atomic",
697 pickup_lib => $ou_id ,
698 fulfillment_time => undef,
701 { order_by => { ahr => "request_time" } }
704 if ( ! $self->api_name =~ /id_list/ ) {
705 flesh_hold_transits($holds);
709 return [ map { $_->id } @$holds ];
713 __PACKAGE__->register_method(
714 method => "uncancel_hold",
715 api_name => "open-ils.circ.hold.uncancel"
719 my($self, $client, $auth, $hold_id) = @_;
720 my $e = new_editor(authtoken=>$auth, xact=>1);
721 return $e->die_event unless $e->checkauth;
723 my $hold = $e->retrieve_action_hold_request($hold_id)
724 or return $e->die_event;
725 return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
727 if ($hold->fulfillment_time) {
731 unless ($hold->cancel_time) {
736 # if configured to reset the request time, also reset the expire time
737 if($U->ou_ancestor_setting_value(
738 $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
740 $hold->request_time('now');
741 $hold->expire_time(calculate_expire_time($hold->request_lib));
744 $hold->clear_cancel_time;
745 $hold->clear_cancel_cause;
746 $hold->clear_cancel_note;
747 $hold->clear_shelf_time;
748 $hold->clear_current_copy;
749 $hold->clear_capture_time;
750 $hold->clear_prev_check_time;
751 $hold->clear_shelf_expire_time;
752 $hold->clear_current_shelf_lib;
754 $e->update_action_hold_request($hold) or return $e->die_event;
757 $U->simplereq('open-ils.hold-targeter',
758 'open-ils.hold-targeter.target', {hold => $hold_id});
764 __PACKAGE__->register_method(
765 method => "cancel_hold",
766 api_name => "open-ils.circ.hold.cancel",
768 desc => 'Cancels the specified hold. The login session is the requestor. If the requestor is different from the usr field ' .
769 'on the hold, the requestor must have CANCEL_HOLDS permissions. The hold may be either the hold object or the hold id',
771 {desc => 'Authentication token', type => 'string'},
772 {desc => 'Hold ID', type => 'number'},
773 {desc => 'Cause of Cancellation', type => 'string'},
774 {desc => 'Note', type => 'string'}
777 desc => '1 on success, event on error'
783 my($self, $client, $auth, $holdid, $cause, $note) = @_;
785 my $e = new_editor(authtoken=>$auth, xact=>1);
786 return $e->die_event unless $e->checkauth;
788 my $hold = $e->retrieve_action_hold_request($holdid)
789 or return $e->die_event;
791 if( $e->requestor->id ne $hold->usr ) {
792 return $e->die_event unless $e->allowed('CANCEL_HOLDS');
795 if ($hold->cancel_time) {
800 # If the hold is captured, reset the copy status
801 if( $hold->capture_time and $hold->current_copy ) {
803 my $copy = $e->retrieve_asset_copy($hold->current_copy)
804 or return $e->die_event;
806 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
807 $logger->info("canceling hold $holdid whose item is on the holds shelf");
808 # $logger->info("setting copy to status 'reshelving' on hold cancel");
809 # $copy->status(OILS_COPY_STATUS_RESHELVING);
810 # $copy->editor($e->requestor->id);
811 # $copy->edit_date('now');
812 # $e->update_asset_copy($copy) or return $e->event;
814 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
817 $logger->warn("! canceling hold [$hid] that is in transit");
818 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
821 my $trans = $e->retrieve_action_transit_copy($transid);
822 # Leave the transit alive, but set the copy status to
823 # reshelving so it will be properly reshelved when it gets back home
825 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
826 $e->update_action_transit_copy($trans) or return $e->die_event;
832 $hold->cancel_time('now');
833 $hold->cancel_cause($cause);
834 $hold->cancel_note($note);
835 $e->update_action_hold_request($hold)
836 or return $e->die_event;
840 # re-fetch the hold to pick up the real cancel_time (not "now") for A/T
842 $hold = $e->retrieve_action_hold_request($hold->id) or return $e->die_event;
845 if ($e->requestor->id == $hold->usr) {
846 $U->create_events_for_hook('hold_request.cancel.patron', $hold, $hold->pickup_lib);
848 $U->create_events_for_hook('hold_request.cancel.staff', $hold, $hold->pickup_lib);
854 my $update_hold_desc = 'The login session is the requestor. ' .
855 'If the requestor is different from the usr field on the hold, ' .
856 'the requestor must have UPDATE_HOLDS permissions. ' .
857 'If supplying a hash of hold data, "id" must be included. ' .
858 'The hash is ignored if a hold object is supplied, ' .
859 'so you should supply only one kind of hold data argument.' ;
861 __PACKAGE__->register_method(
862 method => "update_hold",
863 api_name => "open-ils.circ.hold.update",
865 desc => "Updates the specified hold. $update_hold_desc",
867 {desc => 'Authentication token', type => 'string'},
868 {desc => 'Hold Object', type => 'object'},
869 {desc => 'Hash of values to be applied', type => 'object'}
872 desc => 'Hold ID on success, event on error',
878 __PACKAGE__->register_method(
879 method => "batch_update_hold",
880 api_name => "open-ils.circ.hold.update.batch",
883 desc => "Updates the specified hold(s). $update_hold_desc",
885 {desc => 'Authentication token', type => 'string'},
886 {desc => 'Array of hold obejcts', type => 'array' },
887 {desc => 'Array of hashes of values to be applied', type => 'array' }
890 desc => 'Hold ID per success, event per error',
896 my($self, $client, $auth, $hold, $values) = @_;
897 my $e = new_editor(authtoken=>$auth, xact=>1);
898 return $e->die_event unless $e->checkauth;
899 my $resp = update_hold_impl($self, $e, $hold, $values);
900 if ($U->event_code($resp)) {
904 $e->commit; # FIXME: update_hold_impl already does $e->commit ??
908 sub batch_update_hold {
909 my($self, $client, $auth, $hold_list, $values_list) = @_;
910 my $e = new_editor(authtoken=>$auth);
911 return $e->die_event unless $e->checkauth;
913 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.
915 $values_list ||= []; # FIXME: either move this above $count declaration, or send an event if both lists undef. Probably the latter.
917 # FIXME: Failing over to [] guarantees warnings for "Use of unitialized value" in update_hold_impl call.
918 # FIXME: We should be sure we only call update_hold_impl with hold object OR hash, not both.
920 for my $idx (0..$count-1) {
922 my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
923 $e->xact_commit unless $U->event_code($resp);
924 $client->respond($resp);
928 return undef; # not in the register return type, assuming we should always have at least one list populated
931 sub update_hold_impl {
932 my($self, $e, $hold, $values) = @_;
934 my $need_retarget = 0;
937 $hold = $e->retrieve_action_hold_request($values->{id})
938 or return $e->die_event;
939 for my $k (keys %$values) {
940 # Outside of pickup_lib (covered by the first regex) I don't know when these would currently change.
941 # But hey, why not cover things that may happen later?
942 if ($k =~ '_(lib|ou)$' || $k eq 'target' || $k eq 'hold_type' || $k eq 'requestor' || $k eq 'selection_depth' || $k eq 'holdable_formats') {
943 if (defined $values->{$k} && defined $hold->$k() && $values->{$k} ne $hold->$k()) {
944 # Value changed? RETARGET!
946 } elsif (defined $hold->$k() != defined $values->{$k}) {
947 # Value being set or cleared? RETARGET!
951 if (defined $values->{$k}) {
952 $hold->$k($values->{$k});
954 my $f = "clear_$k"; $hold->$f();
959 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
960 or return $e->die_event;
962 # don't allow the user to be changed
963 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
965 if($hold->usr ne $e->requestor->id) {
966 # if the hold is for a different user, make sure the
967 # requestor has the appropriate permissions
968 my $usr = $e->retrieve_actor_user($hold->usr)
969 or return $e->die_event;
970 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
974 # --------------------------------------------------------------
975 # Changing the request time is like playing God
976 # --------------------------------------------------------------
977 if($hold->request_time ne $orig_hold->request_time) {
978 return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
979 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
983 # --------------------------------------------------------------
984 # Code for making sure staff have appropriate permissons for cut_in_line
985 # This, as is, doesn't prevent a user from cutting their own holds in line
987 # --------------------------------------------------------------
988 if($U->is_true($hold->cut_in_line) ne $U->is_true($orig_hold->cut_in_line)) {
989 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
993 # --------------------------------------------------------------
994 # Disallow hold suspencion if the hold is already captured.
995 # --------------------------------------------------------------
996 if ($U->is_true($hold->frozen) and not $U->is_true($orig_hold->frozen)) {
997 $hold_status = _hold_status($e, $hold);
998 if ($hold_status > 2 && $hold_status != 7) { # hold is captured
999 $logger->info("bypassing hold freeze on captured hold");
1000 return OpenILS::Event->new('HOLD_SUSPEND_AFTER_CAPTURE');
1005 # --------------------------------------------------------------
1006 # if the hold is on the holds shelf or in transit and the pickup
1007 # lib changes we need to create a new transit.
1008 # --------------------------------------------------------------
1009 if($orig_hold->pickup_lib ne $hold->pickup_lib) {
1011 $hold_status = _hold_status($e, $hold) unless $hold_status;
1013 if($hold_status == 3) { # in transit
1015 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
1016 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
1018 $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
1020 # update the transit to reflect the new pickup location
1021 my $transit = $e->search_action_hold_transit_copy(
1022 {hold=>$hold->id, cancel_time => undef, dest_recv_time => undef})->[0]
1023 or return $e->die_event;
1025 $transit->prev_dest($transit->dest); # mark the previous destination on the transit
1026 $transit->dest($hold->pickup_lib);
1027 $e->update_action_hold_transit_copy($transit) or return $e->die_event;
1029 } elsif($hold_status == 4 or $hold_status == 5 or $hold_status == 8) { # on holds shelf
1031 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
1032 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
1034 $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
1036 if ($hold->pickup_lib eq $orig_hold->current_shelf_lib) {
1037 # This can happen if the pickup lib is changed while the hold is
1038 # on the shelf, then changed back to the original pickup lib.
1039 # Restore the original shelf_expire_time to prevent abuse.
1040 set_hold_shelf_expire_time(undef, $hold, $e, $hold->shelf_time);
1043 # clear to prevent premature shelf expiration
1044 $hold->clear_shelf_expire_time;
1049 if($U->is_true($hold->frozen)) {
1050 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1051 $hold->clear_current_copy;
1052 $hold->clear_prev_check_time;
1053 # Clear expire_time to prevent frozen holds from expiring.
1054 $logger->info("clearing expire_time for frozen hold ".$hold->id);
1055 $hold->clear_expire_time;
1058 # If the hold_expire_time is in the past && is not equal to the
1059 # original expire_time, then reset the expire time to be in the
1061 if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1 && $U->datecmp($hold->expire_time, $orig_hold->expire_time) != 0) {
1062 $hold->expire_time(calculate_expire_time($hold->request_lib));
1065 # If the hold is reactivated, reset the expire_time.
1066 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1067 $logger->info("Reset expire_time on activated hold ".$hold->id);
1068 $hold->expire_time(calculate_expire_time($hold->request_lib));
1071 $e->update_action_hold_request($hold) or return $e->die_event;
1074 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1075 $logger->info("Running targeter on activated hold ".$hold->id);
1076 $U->simplereq('open-ils.hold-targeter',
1077 'open-ils.hold-targeter.target', {hold => $hold->id});
1080 # a change to mint-condition changes the set of potential copies, so retarget the hold;
1081 if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
1082 _reset_hold($self, $e->requestor, $hold)
1083 } elsif($need_retarget && !defined $hold->capture_time()) { # If needed, retarget the hold due to changes
1084 $U->simplereq('open-ils.hold-targeter',
1085 'open-ils.hold-targeter.target', {hold => $hold->id});
1091 # this does not update the hold in the DB. It only
1092 # sets the shelf_expire_time field on the hold object.
1093 # start_time is optional and defaults to 'now'
1094 sub set_hold_shelf_expire_time {
1095 my ($class, $hold, $editor, $start_time) = @_;
1097 my $shelf_expire = $U->ou_ancestor_setting_value(
1099 'circ.holds.default_shelf_expire_interval',
1103 return undef unless $shelf_expire;
1105 $start_time = ($start_time) ?
1106 DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time)) :
1107 DateTime->now(time_zone => 'local'); # without time_zone we get UTC ... yuck!
1109 my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($shelf_expire);
1110 my $expire_time = $start_time->add(seconds => $seconds);
1112 # if the shelf expire time overlaps with a pickup lib's
1113 # closed date, push it out to the first open date
1114 my $dateinfo = $U->storagereq(
1115 'open-ils.storage.actor.org_unit.closed_date.overlap',
1116 $hold->pickup_lib, $expire_time->strftime('%FT%T%z'));
1119 my $dt_parser = DateTime::Format::ISO8601->new;
1120 $expire_time = $dt_parser->parse_datetime(clean_ISO8601($dateinfo->{end}));
1122 # TODO: enable/disable time bump via setting?
1123 $expire_time->set(hour => '23', minute => '59', second => '59');
1125 $logger->info("circulator: shelf_expire_time overlaps".
1126 " with closed date, pushing expire time to $expire_time");
1129 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
1135 my($e, $orig_hold, $hold, $copy) = @_;
1136 my $src = $orig_hold->pickup_lib;
1137 my $dest = $hold->pickup_lib;
1139 $logger->info("putting hold into transit on pickup_lib update");
1141 my $transit = Fieldmapper::action::hold_transit_copy->new;
1142 $transit->hold($hold->id);
1143 $transit->source($src);
1144 $transit->dest($dest);
1145 $transit->target_copy($copy->id);
1146 $transit->source_send_time('now');
1147 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1149 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1150 $copy->editor($e->requestor->id);
1151 $copy->edit_date('now');
1153 $e->create_action_hold_transit_copy($transit) or return $e->die_event;
1154 $e->update_asset_copy($copy) or return $e->die_event;
1158 # if the hold is frozen, this method ensures that the hold is not "targeted",
1159 # that is, it clears the current_copy and prev_check_time to essentiallly
1160 # reset the hold. If it is being activated, it runs the targeter in the background
1161 sub update_hold_if_frozen {
1162 my($self, $e, $hold, $orig_hold) = @_;
1163 return if $hold->capture_time;
1165 if($U->is_true($hold->frozen)) {
1166 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1167 $hold->clear_current_copy;
1168 $hold->clear_prev_check_time;
1171 if($U->is_true($orig_hold->frozen)) {
1172 $logger->info("Running targeter on activated hold ".$hold->id);
1173 $U->simplereq('open-ils.hold-targeter',
1174 'open-ils.hold-targeter.target', {hold => $hold->id});
1179 __PACKAGE__->register_method(
1180 method => "hold_note_CUD",
1181 api_name => "open-ils.circ.hold_request.note.cud",
1183 desc => 'Create, update or delete a hold request note. If the operator (from Auth. token) '
1184 . 'is not the owner of the hold, the UPDATE_HOLD permission is required',
1186 { desc => 'Authentication token', type => 'string' },
1187 { desc => 'Hold note object', type => 'object' }
1190 desc => 'Returns the note ID, event on error'
1196 my($self, $conn, $auth, $note) = @_;
1198 my $e = new_editor(authtoken => $auth, xact => 1);
1199 return $e->die_event unless $e->checkauth;
1201 my $hold = $e->retrieve_action_hold_request($note->hold)
1202 or return $e->die_event;
1204 if($hold->usr ne $e->requestor->id) {
1205 my $usr = $e->retrieve_actor_user($hold->usr);
1206 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1207 $note->staff('t') if $note->isnew;
1211 $e->create_action_hold_request_note($note) or return $e->die_event;
1212 } elsif($note->ischanged) {
1213 $e->update_action_hold_request_note($note) or return $e->die_event;
1214 } elsif($note->isdeleted) {
1215 $e->delete_action_hold_request_note($note) or return $e->die_event;
1223 __PACKAGE__->register_method(
1224 method => "retrieve_hold_status",
1225 api_name => "open-ils.circ.hold.status.retrieve",
1227 desc => 'Calculates the current status of the hold. The requestor must have ' .
1228 'VIEW_HOLD permissions if the hold is for a user other than the requestor' ,
1230 { desc => 'Hold ID', type => 'number' }
1233 # type => 'number', # event sometimes
1234 desc => <<'END_OF_DESC'
1235 Returns event on error or:
1236 -1 on error (for now),
1237 1 for 'waiting for copy to become available',
1238 2 for 'waiting for copy capture',
1241 5 for 'hold-shelf-delay'
1244 8 for 'captured, on wrong hold shelf'
1251 sub retrieve_hold_status {
1252 my($self, $client, $auth, $hold_id) = @_;
1254 my $e = new_editor(authtoken => $auth);
1255 return $e->event unless $e->checkauth;
1256 my $hold = $e->retrieve_action_hold_request($hold_id)
1257 or return $e->event;
1259 if( $e->requestor->id != $hold->usr ) {
1260 return $e->event unless $e->allowed('VIEW_HOLD');
1263 return _hold_status($e, $hold);
1269 if ($hold->cancel_time) {
1272 if ($U->is_true($hold->frozen) && !$hold->capture_time) {
1275 if ($hold->current_shelf_lib and $hold->current_shelf_lib ne $hold->pickup_lib) {
1278 if ($hold->fulfillment_time) {
1281 return 1 unless $hold->current_copy;
1282 return 2 unless $hold->capture_time;
1284 my $copy = $hold->current_copy;
1285 unless( ref $copy ) {
1286 $copy = $e->retrieve_asset_copy($hold->current_copy)
1287 or return $e->event;
1290 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
1292 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
1294 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
1295 return 4 unless $hs_wait_interval;
1297 # if a hold_shelf_status_delay interval is defined and start_time plus
1298 # the interval is greater than now, consider the hold to be in the virtual
1299 # "on its way to the holds shelf" status. Return 5.
1301 my $transit = $e->search_action_hold_transit_copy({
1303 target_copy => $copy->id,
1304 cancel_time => undef,
1305 dest_recv_time => {'!=' => undef},
1307 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
1308 $start_time = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time));
1309 my $end_time = $start_time->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($hs_wait_interval));
1311 return 5 if $end_time > DateTime->now;
1320 __PACKAGE__->register_method(
1321 method => "retrieve_hold_queue_stats",
1322 api_name => "open-ils.circ.hold.queue_stats.retrieve",
1324 desc => 'Returns summary data about the state of a hold',
1326 { desc => 'Authentication token', type => 'string'},
1327 { desc => 'Hold ID', type => 'number'},
1330 desc => q/Summary object with keys:
1331 total_holds : total holds in queue
1332 queue_position : current queue position
1333 potential_copies : number of potential copies for this hold
1334 estimated_wait : estimated wait time in days
1335 status : hold status
1336 -1 => error or unexpected state,
1337 1 => 'waiting for copy to become available',
1338 2 => 'waiting for copy capture',
1341 5 => 'hold-shelf-delay'
1348 sub retrieve_hold_queue_stats {
1349 my($self, $conn, $auth, $hold_id) = @_;
1350 my $e = new_editor(authtoken => $auth);
1351 return $e->event unless $e->checkauth;
1352 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
1353 if($e->requestor->id != $hold->usr) {
1354 return $e->event unless $e->allowed('VIEW_HOLD');
1356 return retrieve_hold_queue_status_impl($e, $hold);
1359 sub retrieve_hold_queue_status_impl {
1363 # The holds queue is defined as the distinct set of holds that share at
1364 # least one potential copy with the context hold, plus any holds that
1365 # share the same hold type and target. The latter part exists to
1366 # accomodate holds that currently have no potential copies
1367 my $q_holds = $e->json_query({
1369 # fetch cut_in_line and request_time since they're in the order_by
1370 # and we're asking for distinct values
1371 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1375 select => { ahcm => ['hold'] },
1380 'field' => 'target_copy',
1381 'fkey' => 'target_copy'
1385 where => { '+ahcm2' => { hold => $hold->id } },
1392 "field" => "cut_in_line",
1393 "transform" => "coalesce",
1395 "direction" => "desc"
1397 { "class" => "ahr", "field" => "request_time" }
1402 if (!@$q_holds) { # none? maybe we don't have a map ...
1403 $q_holds = $e->json_query({
1404 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1409 "field" => "cut_in_line",
1410 "transform" => "coalesce",
1412 "direction" => "desc"
1414 { "class" => "ahr", "field" => "request_time" }
1417 hold_type => $hold->hold_type,
1418 target => $hold->target,
1419 capture_time => undef,
1420 cancel_time => undef,
1422 {expire_time => undef },
1423 {expire_time => {'>' => 'now'}}
1431 for my $h (@$q_holds) {
1432 last if $h->{id} == $hold->id;
1436 my $hold_data = $e->json_query({
1438 acp => [ {column => 'id', transform => 'count', aggregate => 1, alias => 'count'} ],
1439 ccm => [ {column =>'avg_wait_time'} ]
1445 ccm => {type => 'left'}
1450 where => {'+ahcm' => {hold => $hold->id} }
1453 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1455 my $default_wait = $U->ou_ancestor_setting_value(
1456 $user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL, $e);
1457 my $min_wait = $U->ou_ancestor_setting_value(
1458 $user_org, 'circ.holds.min_estimated_wait_interval', $e);
1459 $min_wait = OpenILS::Utils::DateTime->interval_to_seconds($min_wait || '0 seconds');
1460 $default_wait ||= '0 seconds';
1462 # Estimated wait time is the average wait time across the set
1463 # of potential copies, divided by the number of potential copies
1464 # times the queue position.
1466 my $combined_secs = 0;
1467 my $num_potentials = 0;
1469 for my $wait_data (@$hold_data) {
1470 my $count += $wait_data->{count};
1471 $combined_secs += $count *
1472 OpenILS::Utils::DateTime->interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
1473 $num_potentials += $count;
1476 my $estimated_wait = -1;
1478 if($num_potentials) {
1479 my $avg_wait = $combined_secs / $num_potentials;
1480 $estimated_wait = $qpos * ($avg_wait / $num_potentials);
1481 $estimated_wait = $min_wait if $estimated_wait < $min_wait and $estimated_wait != -1;
1485 total_holds => scalar(@$q_holds),
1486 queue_position => $qpos,
1487 potential_copies => $num_potentials,
1488 status => _hold_status( $e, $hold ),
1489 estimated_wait => int($estimated_wait)
1494 sub fetch_open_hold_by_current_copy {
1497 my $hold = $apputils->simplereq(
1499 'open-ils.cstore.direct.action.hold_request.search.atomic',
1500 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1501 return $hold->[0] if ref($hold);
1505 sub fetch_related_holds {
1508 return $apputils->simplereq(
1510 'open-ils.cstore.direct.action.hold_request.search.atomic',
1511 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1515 __PACKAGE__->register_method(
1516 method => "hold_pull_list",
1517 api_name => "open-ils.circ.hold_pull_list.retrieve",
1519 desc => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1520 'The location is determined by the login session.',
1522 { desc => 'Limit (optional)', type => 'number'},
1523 { desc => 'Offset (optional)', type => 'number'},
1526 desc => 'reference to a list of holds, or event on failure',
1531 __PACKAGE__->register_method(
1532 method => "hold_pull_list",
1533 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
1535 desc => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
1536 'The location is determined by the login session.',
1538 { desc => 'Limit (optional)', type => 'number'},
1539 { desc => 'Offset (optional)', type => 'number'},
1542 desc => 'reference to a list of holds, or event on failure',
1547 __PACKAGE__->register_method(
1548 method => "hold_pull_list",
1549 api_name => "open-ils.circ.hold_pull_list.retrieve.count",
1551 desc => 'Returns a count of holds that need to be "pulled" by a given location. ' .
1552 'The location is determined by the login session.',
1554 { desc => 'Limit (optional)', type => 'number'},
1555 { desc => 'Offset (optional)', type => 'number'},
1558 desc => 'Holds count (integer), or event on failure',
1564 __PACKAGE__->register_method(
1565 method => "hold_pull_list",
1567 # TODO: tag with api_level 2 once fully supported
1568 api_name => "open-ils.circ.hold_pull_list.fleshed.stream",
1570 desc => q/Returns a stream of fleshed holds that need to be
1571 "pulled" by a given location. The location is
1572 determined by the login session.
1573 This API calls always run in authoritative mode./,
1575 { desc => 'Limit (optional)', type => 'number'},
1576 { desc => 'Offset (optional)', type => 'number'},
1579 desc => 'Stream of holds holds, or event on failure',
1584 sub hold_pull_list {
1585 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1586 my( $reqr, $evt ) = $U->checkses($authtoken);
1587 return $evt if $evt;
1589 my $org = $reqr->ws_ou || $reqr->home_ou;
1590 # the perm locaiton shouldn't really matter here since holds
1591 # will exist all over and VIEW_HOLDS should be universal
1592 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1593 return $evt if $evt;
1595 if($self->api_name =~ /count/) {
1597 my $count = $U->storagereq(
1598 'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1599 $org, $limit, $offset );
1601 $logger->info("Grabbing pull list for org unit $org with $count items");
1604 } elsif( $self->api_name =~ /id_list/ ) {
1606 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1607 $org, $limit, $offset );
1609 } elsif ($self->api_name =~ /fleshed/) {
1611 my $ids = $U->storagereq(
1612 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1613 $org, $limit, $offset );
1615 my $e = new_editor(xact => 1, requestor => $reqr);
1616 $conn->respond(uber_hold_impl($e, $_, {flesh_acpl => 1})) for @$ids;
1618 $conn->respond_complete;
1623 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1624 $org, $limit, $offset );
1628 __PACKAGE__->register_method(
1629 method => "print_hold_pull_list",
1630 api_name => "open-ils.circ.hold_pull_list.print",
1632 desc => 'Returns an HTML-formatted holds pull list',
1634 { desc => 'Authtoken', type => 'string'},
1635 { desc => 'Org unit ID. Optional, defaults to workstation org unit', type => 'number'},
1638 desc => 'HTML string',
1644 sub print_hold_pull_list {
1645 my($self, $client, $auth, $org_id) = @_;
1647 my $e = new_editor(authtoken=>$auth);
1648 return $e->event unless $e->checkauth;
1650 $org_id = (defined $org_id) ? $org_id : $e->requestor->ws_ou;
1651 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1653 my $hold_ids = $U->storagereq(
1654 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1657 return undef unless @$hold_ids;
1659 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1661 # Holds will /NOT/ be in order after this ...
1662 my $holds = $e->search_action_hold_request({id => $hold_ids}, {substream => 1});
1663 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1665 # ... so we must resort.
1666 my $hold_map = +{map { $_->id => $_ } @$holds};
1667 my $sorted_holds = [];
1668 push @$sorted_holds, $hold_map->{$_} foreach @$hold_ids;
1670 return $U->fire_object_event(
1671 undef, "ahr.format.pull_list", $sorted_holds,
1672 $org_id, undef, undef, $client
1677 __PACKAGE__->register_method(
1678 method => "print_hold_pull_list_stream",
1680 api_name => "open-ils.circ.hold_pull_list.print.stream",
1682 desc => 'Returns a stream of fleshed holds',
1684 { desc => 'Authtoken', type => 'string'},
1685 { 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)',
1690 desc => 'A stream of fleshed holds',
1696 sub print_hold_pull_list_stream {
1697 my($self, $client, $auth, $params) = @_;
1699 my $e = new_editor(authtoken=>$auth);
1700 return $e->die_event unless $e->checkauth;
1702 delete($$params{org_id}) unless (int($$params{org_id}));
1703 delete($$params{limit}) unless (int($$params{limit}));
1704 delete($$params{offset}) unless (int($$params{offset}));
1705 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
1706 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
1707 $$params{chunk_size} ||= 10;
1708 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
1710 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
1711 return $e->die_event unless $e->allowed('VIEW_HOLD', $$params{org_id });
1714 if ($$params{sort} && @{ $$params{sort} }) {
1715 for my $s (@{ $$params{sort} }) {
1716 if ($s eq 'acplo.position') {
1718 "class" => "acplo", "field" => "position",
1719 "transform" => "coalesce", "params" => [999]
1721 } elsif ($s eq 'prefix') {
1722 push @$sort, {"class" => "acnp", "field" => "label_sortkey"};
1723 } elsif ($s eq 'call_number') {
1724 push @$sort, {"class" => "acn", "field" => "label_sortkey"};
1725 } elsif ($s eq 'suffix') {
1726 push @$sort, {"class" => "acns", "field" => "label_sortkey"};
1727 } elsif ($s eq 'request_time') {
1728 push @$sort, {"class" => "ahr", "field" => "request_time"};
1732 push @$sort, {"class" => "ahr", "field" => "request_time"};
1735 my $holds_ids = $e->json_query(
1737 "select" => {"ahr" => ["id"]},
1742 "fkey" => "current_copy",
1744 "circ_lib" => $$params{org_id}, "status" => [0,7]
1749 "fkey" => "call_number",
1763 "fkey" => "circ_lib",
1766 "location" => {"=" => {"+acp" => "location"}}
1775 "capture_time" => undef,
1776 "cancel_time" => undef,
1778 {"expire_time" => undef },
1779 {"expire_time" => {">" => "now"}}
1783 (@$sort ? (order_by => $sort) : ()),
1784 ($$params{limit} ? (limit => $$params{limit}) : ()),
1785 ($$params{offset} ? (offset => $$params{offset}) : ())
1786 }, {"substream" => 1}
1787 ) or return $e->die_event;
1789 $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
1792 for my $hid (@$holds_ids) {
1793 push @chunk, $e->retrieve_action_hold_request([
1797 "ahr" => ["usr", "current_copy"],
1799 "acp" => ["location", "call_number", "parts"],
1800 "acn" => ["record","prefix","suffix"]
1805 if (@chunk >= $$params{chunk_size}) {
1806 $client->respond( \@chunk );
1810 $client->respond_complete( \@chunk ) if (@chunk);
1817 __PACKAGE__->register_method(
1818 method => 'fetch_hold_notify',
1819 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
1822 Returns a list of hold notification objects based on hold id.
1823 @param authtoken The loggin session key
1824 @param holdid The id of the hold whose notifications we want to retrieve
1825 @return An array of hold notification objects, event on error.
1829 sub fetch_hold_notify {
1830 my( $self, $conn, $authtoken, $holdid ) = @_;
1831 my( $requestor, $evt ) = $U->checkses($authtoken);
1832 return $evt if $evt;
1833 my ($hold, $patron);
1834 ($hold, $evt) = $U->fetch_hold($holdid);
1835 return $evt if $evt;
1836 ($patron, $evt) = $U->fetch_user($hold->usr);
1837 return $evt if $evt;
1839 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
1840 return $evt if $evt;
1842 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
1843 return $U->cstorereq(
1844 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
1848 __PACKAGE__->register_method(
1849 method => 'create_hold_notify',
1850 api_name => 'open-ils.circ.hold_notification.create',
1852 Creates a new hold notification object
1853 @param authtoken The login session key
1854 @param notification The hold notification object to create
1855 @return ID of the new object on success, Event on error
1859 sub create_hold_notify {
1860 my( $self, $conn, $auth, $note ) = @_;
1861 my $e = new_editor(authtoken=>$auth, xact=>1);
1862 return $e->die_event unless $e->checkauth;
1864 my $hold = $e->retrieve_action_hold_request($note->hold)
1865 or return $e->die_event;
1866 my $patron = $e->retrieve_actor_user($hold->usr)
1867 or return $e->die_event;
1869 return $e->die_event unless
1870 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
1872 $note->notify_staff($e->requestor->id);
1873 $e->create_action_hold_notification($note) or return $e->die_event;
1878 __PACKAGE__->register_method(
1879 method => 'create_hold_note',
1880 api_name => 'open-ils.circ.hold_note.create',
1882 Creates a new hold request note object
1883 @param authtoken The login session key
1884 @param note The hold note object to create
1885 @return ID of the new object on success, Event on error
1889 sub create_hold_note {
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('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
1902 $e->create_action_hold_request_note($note) or return $e->die_event;
1907 __PACKAGE__->register_method(
1908 method => 'reset_hold',
1909 api_name => 'open-ils.circ.hold.reset',
1911 Un-captures and un-targets a hold, essentially returning
1912 it to the state it was in directly after it was placed,
1913 then attempts to re-target the hold
1914 @param authtoken The login session key
1915 @param holdid The id of the hold
1921 my( $self, $conn, $auth, $holdid ) = @_;
1923 my ($hold, $evt) = $U->fetch_hold($holdid);
1924 return $evt if $evt;
1925 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1926 return $evt if $evt;
1927 $evt = _reset_hold($self, $reqr, $hold);
1928 return $evt if $evt;
1933 __PACKAGE__->register_method(
1934 method => 'reset_hold_batch',
1935 api_name => 'open-ils.circ.hold.reset.batch'
1938 sub reset_hold_batch {
1939 my($self, $conn, $auth, $hold_ids) = @_;
1941 my $e = new_editor(authtoken => $auth);
1942 return $e->event unless $e->checkauth;
1944 for my $hold_id ($hold_ids) {
1946 my $hold = $e->retrieve_action_hold_request(
1947 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
1948 or return $e->event;
1950 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1951 _reset_hold($self, $e->requestor, $hold);
1959 my ($self, $reqr, $hold) = @_;
1961 my $e = new_editor(xact =>1, requestor => $reqr);
1963 $logger->info("reseting hold ".$hold->id);
1965 my $hid = $hold->id;
1967 if( $hold->capture_time and $hold->current_copy ) {
1969 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1970 or return $e->die_event;
1972 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1973 $logger->info("setting copy to status 'reshelving' on hold retarget");
1974 $copy->status(OILS_COPY_STATUS_RESHELVING);
1975 $copy->editor($e->requestor->id);
1976 $copy->edit_date('now');
1977 $e->update_asset_copy($copy) or return $e->die_event;
1979 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1981 $logger->warn("! reseting hold [$hid] that is in transit");
1982 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
1985 my $trans = $e->retrieve_action_transit_copy($transid);
1987 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1988 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
1989 $logger->info("Transit abort completed with result $evt");
1990 unless ("$evt" eq 1) {
1999 $hold->clear_capture_time;
2000 $hold->clear_current_copy;
2001 $hold->clear_shelf_time;
2002 $hold->clear_shelf_expire_time;
2003 $hold->clear_current_shelf_lib;
2005 $e->update_action_hold_request($hold) or return $e->die_event;
2008 $U->simplereq('open-ils.hold-targeter',
2009 'open-ils.hold-targeter.target', {hold => $hold->id});
2015 __PACKAGE__->register_method(
2016 method => 'fetch_open_title_holds',
2017 api_name => 'open-ils.circ.open_holds.retrieve',
2019 Returns a list ids of un-fulfilled holds for a given title id
2020 @param authtoken The login session key
2021 @param id the id of the item whose holds we want to retrieve
2022 @param type The hold type - M, T, I, V, C, F, R
2026 sub fetch_open_title_holds {
2027 my( $self, $conn, $auth, $id, $type, $org ) = @_;
2028 my $e = new_editor( authtoken => $auth );
2029 return $e->event unless $e->checkauth;
2032 $org ||= $e->requestor->ws_ou;
2034 # return $e->search_action_hold_request(
2035 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
2037 # XXX make me return IDs in the future ^--
2038 my $holds = $e->search_action_hold_request(
2041 cancel_time => undef,
2043 fulfillment_time => undef
2047 flesh_hold_transits($holds);
2052 sub flesh_hold_transits {
2054 for my $hold ( @$holds ) {
2056 $apputils->simplereq(
2058 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
2059 { hold => $hold->id, cancel_time => undef },
2060 { order_by => { ahtc => 'id desc' }, limit => 1 }
2066 sub flesh_hold_notices {
2067 my( $holds, $e ) = @_;
2068 $e ||= new_editor();
2070 for my $hold (@$holds) {
2071 my $notices = $e->search_action_hold_notification(
2073 { hold => $hold->id },
2074 { order_by => { anh => 'notify_time desc' } },
2079 $hold->notify_count(scalar(@$notices));
2081 my $n = $e->retrieve_action_hold_notification($$notices[0])
2082 or return $e->event;
2083 $hold->notify_time($n->notify_time);
2089 __PACKAGE__->register_method(
2090 method => 'fetch_captured_holds',
2091 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
2095 Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
2096 @param authtoken The login session key
2097 @param org The org id of the location in question
2098 @param match_copy A specific copy to limit to
2102 __PACKAGE__->register_method(
2103 method => 'fetch_captured_holds',
2104 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
2108 Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
2109 @param authtoken The login session key
2110 @param org The org id of the location in question
2111 @param match_copy A specific copy to limit to
2115 __PACKAGE__->register_method(
2116 method => 'fetch_captured_holds',
2117 api_name => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
2121 Returns list ids of shelf-expired un-fulfilled holds for a given title id
2122 @param authtoken The login session key
2123 @param org The org id of the location in question
2124 @param match_copy A specific copy to limit to
2128 __PACKAGE__->register_method(
2129 method => 'fetch_captured_holds',
2131 'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
2135 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2136 for a given shelf lib
2140 __PACKAGE__->register_method(
2141 method => 'fetch_captured_holds',
2143 'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
2147 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2148 for a given shelf lib
2153 sub fetch_captured_holds {
2154 my( $self, $conn, $auth, $org, $match_copy ) = @_;
2156 my $e = new_editor(authtoken => $auth);
2157 return $e->die_event unless $e->checkauth;
2158 return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
2160 $org ||= $e->requestor->ws_ou;
2162 my $current_copy = { '!=' => undef };
2163 $current_copy = { '=' => $match_copy } if $match_copy;
2166 select => { alhr => ['id'] },
2171 fkey => 'current_copy'
2176 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
2178 capture_time => { "!=" => undef },
2179 current_copy => $current_copy,
2180 fulfillment_time => undef,
2181 current_shelf_lib => $org
2185 if($self->api_name =~ /expired/) {
2186 $query->{'where'}->{'+alhr'}->{'-or'} = {
2187 shelf_expire_time => { '<' => 'today'},
2188 cancel_time => { '!=' => undef },
2191 my $hold_ids = $e->json_query( $query );
2193 if ($self->api_name =~ /wrong_shelf/) {
2194 # fetch holds whose current_shelf_lib is $org, but whose pickup
2195 # lib is some other org unit. Ignore already-retrieved holds.
2197 pickup_lib_changed_on_shelf_holds(
2198 $e, $org, [map {$_->{id}} @$hold_ids]);
2199 # match the layout of other items in $hold_ids
2200 push (@$hold_ids, {id => $_}) for @$wrong_shelf;
2204 for my $hold_id (@$hold_ids) {
2205 if($self->api_name =~ /id_list/) {
2206 $conn->respond($hold_id->{id});
2210 $e->retrieve_action_hold_request([
2214 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
2215 order_by => {anh => 'notify_time desc'}
2225 __PACKAGE__->register_method(
2226 method => "print_expired_holds_stream",
2227 api_name => "open-ils.circ.captured_holds.expired.print.stream",
2231 sub print_expired_holds_stream {
2232 my ($self, $client, $auth, $params) = @_;
2234 # No need to check specific permissions: we're going to call another method
2235 # that will do that.
2236 my $e = new_editor("authtoken" => $auth);
2237 return $e->die_event unless $e->checkauth;
2239 delete($$params{org_id}) unless (int($$params{org_id}));
2240 delete($$params{limit}) unless (int($$params{limit}));
2241 delete($$params{offset}) unless (int($$params{offset}));
2242 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2243 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2244 $$params{chunk_size} ||= 10;
2245 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2247 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2249 my @hold_ids = $self->method_lookup(
2250 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
2251 )->run($auth, $params->{"org_id"});
2256 } elsif (defined $U->event_code($hold_ids[0])) {
2258 return $hold_ids[0];
2261 $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
2264 my @hid_chunk = splice @hold_ids, 0, $params->{"chunk_size"};
2266 my $result_chunk = $e->json_query({
2268 "acp" => ["barcode"],
2270 first_given_name second_given_name family_name alias
2279 "field" => "id", "fkey" => "current_copy",
2282 "field" => "id", "fkey" => "call_number",
2285 "field" => "id", "fkey" => "record"
2289 "acpl" => {"field" => "id", "fkey" => "location"}
2292 "au" => {"field" => "id", "fkey" => "usr"}
2295 "where" => {"+ahr" => {"id" => \@hid_chunk}}
2296 }) or return $e->die_event;
2297 $client->respond($result_chunk);
2304 __PACKAGE__->register_method(
2305 method => "check_title_hold_batch",
2306 api_name => "open-ils.circ.title_hold.is_possible.batch",
2309 desc => '@see open-ils.circ.title_hold.is_possible.batch',
2311 { desc => 'Authentication token', type => 'string'},
2312 { desc => 'Array of Hash of named parameters', type => 'array'},
2315 desc => 'Array of response objects',
2321 sub check_title_hold_batch {
2322 my($self, $client, $authtoken, $param_list, $oargs) = @_;
2323 foreach (@$param_list) {
2324 my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
2325 $client->respond($res);
2331 __PACKAGE__->register_method(
2332 method => "check_title_hold",
2333 api_name => "open-ils.circ.title_hold.is_possible",
2335 desc => 'Determines if a hold were to be placed by a given user, ' .
2336 'whether or not said hold would have any potential copies to fulfill it.' .
2337 'The named paramaters of the second argument include: ' .
2338 'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
2339 'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
2341 { desc => 'Authentication token', type => 'string'},
2342 { desc => 'Hash of named parameters', type => 'object'},
2345 desc => 'List of new message IDs (empty if none)',
2351 =head3 check_title_hold (token, hash)
2353 The named fields in the hash are:
2355 patronid - ID of the hold recipient (required)
2356 depth - hold range depth (default 0)
2357 pickup_lib - destination for hold, fallback value for selection_ou
2358 selection_ou - ID of org_unit establishing hard and soft hold boundary settings
2359 issuanceid - ID of the issuance to be held, required for Issuance level hold
2360 partid - ID of the monograph part to be held, required for monograph part level hold
2361 titleid - ID (BRN) of the title to be held, required for Title level hold
2362 volume_id - required for Volume level hold
2363 copy_id - required for Copy level hold
2364 mrid - required for Meta-record level hold
2365 hold_type - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record (default "T")
2367 All key/value pairs are passed on to do_possibility_checks.
2371 # FIXME: better params checking. what other params are required, if any?
2372 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
2373 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
2374 # used in conditionals, where it may be undefined, causing a warning.
2375 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
2377 sub check_title_hold {
2378 my( $self, $client, $authtoken, $params ) = @_;
2379 my $e = new_editor(authtoken=>$authtoken);
2380 return $e->event unless $e->checkauth;
2382 my %params = %$params;
2383 my $depth = $params{depth} || 0;
2384 $params{depth} = $depth; #define $params{depth} if unset, since it gets used later
2385 my $selection_ou = $params{selection_ou} || $params{pickup_lib};
2386 my $oargs = $params{oargs} || {};
2388 if($oargs->{events}) {
2389 @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
2393 my $patron = $e->retrieve_actor_user($params{patronid})
2394 or return $e->event;
2396 if( $e->requestor->id ne $patron->id ) {
2397 return $e->event unless
2398 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
2401 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
2403 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
2404 or return $e->event;
2406 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
2407 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
2410 my $return_depth = $hard_boundary; # default depth to return on success
2411 if(defined $soft_boundary and $depth < $soft_boundary) {
2412 # work up the tree and as soon as we find a potential copy, use that depth
2413 # also, make sure we don't go past the hard boundary if it exists
2415 # our min boundary is the greater of user-specified boundary or hard boundary
2416 my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
2417 $hard_boundary : $depth;
2419 my $depth = $soft_boundary;
2420 while($depth >= $min_depth) {
2421 $logger->info("performing hold possibility check with soft boundary $depth");
2422 @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
2424 $return_depth = $depth;
2429 } elsif(defined $hard_boundary and $depth < $hard_boundary) {
2430 # there is no soft boundary, enforce the hard boundary if it exists
2431 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
2432 @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
2434 # no boundaries defined, fall back to user specifed boundary or no boundary
2435 $logger->info("performing hold possibility check with no boundary");
2436 @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
2439 my $place_unfillable = 0;
2440 $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
2445 "depth" => $return_depth,
2446 "local_avail" => $status[1]
2448 } elsif ($status[2]) {
2449 my $n = scalar @{$status[2]};
2450 return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2452 return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2458 sub do_possibility_checks {
2459 my($e, $patron, $request_lib, $depth, %params) = @_;
2461 my $issuanceid = $params{issuanceid} || "";
2462 my $partid = $params{partid} || "";
2463 my $titleid = $params{titleid} || "";
2464 my $volid = $params{volume_id};
2465 my $copyid = $params{copy_id};
2466 my $mrid = $params{mrid} || "";
2467 my $pickup_lib = $params{pickup_lib};
2468 my $hold_type = $params{hold_type} || 'T';
2469 my $selection_ou = $params{selection_ou} || $pickup_lib;
2470 my $holdable_formats = $params{holdable_formats};
2471 my $oargs = $params{oargs} || {};
2478 if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
2480 return $e->event unless $copy = $e->retrieve_asset_copy($copyid);
2481 return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
2482 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2484 return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
2485 return verify_copy_for_hold(
2486 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
2489 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2491 return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
2492 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2494 return _check_volume_hold_is_possible(
2495 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2498 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
2500 return _check_title_hold_is_possible(
2501 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
2504 } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
2506 return _check_issuance_hold_is_possible(
2507 $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2510 } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
2512 return _check_monopart_hold_is_possible(
2513 $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2516 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2518 # pasing undef as the depth to filtered_records causes the depth
2519 # of the selection_ou to be used, which is not what we want here.
2522 my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
2524 for my $rec (@$recs) {
2525 @status = _check_title_hold_is_possible(
2526 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
2532 # else { Unrecognized hold_type ! } # FIXME: return error? or 0?
2535 sub MR_filter_records {
2542 my $opac_visible = shift;
2544 my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
2545 return $U->storagereq(
2546 'open-ils.storage.metarecord.filtered_records.atomic',
2547 $m, $f, $org_at_depth, $opac_visible
2550 __PACKAGE__->register_method(
2551 method => 'MR_filter_records',
2552 api_name => 'open-ils.circ.holds.metarecord.filtered_records',
2557 sub create_ranged_org_filter {
2558 my($e, $selection_ou, $depth) = @_;
2560 # find the orgs from which this hold may be fulfilled,
2561 # based on the selection_ou and depth
2563 my $top_org = $e->search_actor_org_unit([
2564 {parent_ou => undef},
2565 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
2568 return () if $depth == $top_org->ou_type->depth;
2570 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
2571 %org_filter = (circ_lib => []);
2572 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
2574 $logger->info("hold org filter at depth $depth and selection_ou ".
2575 "$selection_ou created list of @{$org_filter{circ_lib}}");
2581 sub _check_title_hold_is_possible {
2582 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
2583 # $holdable_formats is now unused. We pre-filter the MR's records.
2585 my $e = new_editor();
2586 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2588 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2589 my $copies = $e->json_query(
2591 select => { acp => ['id', 'circ_lib'] },
2596 fkey => 'call_number',
2597 filter => { record => $titleid }
2601 filter => { holdable => 't', deleted => 'f' },
2604 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' },
2605 acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2609 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2610 '+acpm' => { target_copy => undef } # ignore part-linked copies
2615 $logger->info("title possible found ".scalar(@$copies)." potential copies");
2619 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2620 "payload" => {"fail_part" => "no_ultimate_items"}
2625 # -----------------------------------------------------------------------
2626 # sort the copies into buckets based on their circ_lib proximity to
2627 # the patron's home_ou.
2628 # -----------------------------------------------------------------------
2630 my $home_org = $patron->home_ou;
2631 my $req_org = $request_lib->id;
2633 $prox_cache{$home_org} =
2634 $e->search_actor_org_unit_proximity({from_org => $home_org})
2635 unless $prox_cache{$home_org};
2636 my $home_prox = $prox_cache{$home_org};
2637 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2640 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2641 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2643 my @keys = sort { $a <=> $b } keys %buckets;
2646 if( $home_org ne $req_org ) {
2647 # -----------------------------------------------------------------------
2648 # shove the copies close to the request_lib into the primary buckets
2649 # directly before the farthest away copies. That way, they are not
2650 # given priority, but they are checked before the farthest copies.
2651 # -----------------------------------------------------------------------
2652 $prox_cache{$req_org} =
2653 $e->search_actor_org_unit_proximity({from_org => $req_org})
2654 unless $prox_cache{$req_org};
2655 my $req_prox = $prox_cache{$req_org};
2658 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2659 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2661 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2662 my $new_key = $highest_key - 0.5; # right before the farthest prox
2663 my @keys2 = sort { $a <=> $b } keys %buckets2;
2664 for my $key (@keys2) {
2665 last if $key >= $highest_key;
2666 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2670 @keys = sort { $a <=> $b } keys %buckets;
2675 my $age_protect_only = 0;
2676 OUTER: for my $key (@keys) {
2677 my @cps = @{$buckets{$key}};
2679 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2681 for my $copyid (@cps) {
2683 next if $seen{$copyid};
2684 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2685 my $copy = $e->retrieve_asset_copy($copyid);
2686 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2688 unless($title) { # grab the title if we don't already have it
2689 my $vol = $e->retrieve_asset_call_number(
2690 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2691 $title = $vol->record;
2694 @status = verify_copy_for_hold(
2695 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2697 $age_protect_only ||= $status[3];
2698 last OUTER if $status[0];
2702 $status[3] = $age_protect_only;
2706 sub _check_issuance_hold_is_possible {
2707 my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2709 my $e = new_editor();
2710 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2712 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2713 my $copies = $e->json_query(
2715 select => { acp => ['id', 'circ_lib'] },
2721 filter => { issuance => $issuanceid }
2725 filter => { holdable => 't', deleted => 'f' },
2728 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
2732 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
2738 $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
2742 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
2743 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2748 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2749 "payload" => {"fail_part" => "no_ultimate_items"}
2757 # -----------------------------------------------------------------------
2758 # sort the copies into buckets based on their circ_lib proximity to
2759 # the patron's home_ou.
2760 # -----------------------------------------------------------------------
2762 my $home_org = $patron->home_ou;
2763 my $req_org = $request_lib->id;
2765 $prox_cache{$home_org} =
2766 $e->search_actor_org_unit_proximity({from_org => $home_org})
2767 unless $prox_cache{$home_org};
2768 my $home_prox = $prox_cache{$home_org};
2769 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2772 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2773 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2775 my @keys = sort { $a <=> $b } keys %buckets;
2778 if( $home_org ne $req_org ) {
2779 # -----------------------------------------------------------------------
2780 # shove the copies close to the request_lib into the primary buckets
2781 # directly before the farthest away copies. That way, they are not
2782 # given priority, but they are checked before the farthest copies.
2783 # -----------------------------------------------------------------------
2784 $prox_cache{$req_org} =
2785 $e->search_actor_org_unit_proximity({from_org => $req_org})
2786 unless $prox_cache{$req_org};
2787 my $req_prox = $prox_cache{$req_org};
2790 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2791 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2793 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2794 my $new_key = $highest_key - 0.5; # right before the farthest prox
2795 my @keys2 = sort { $a <=> $b } keys %buckets2;
2796 for my $key (@keys2) {
2797 last if $key >= $highest_key;
2798 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2802 @keys = sort { $a <=> $b } keys %buckets;
2807 my $age_protect_only = 0;
2808 OUTER: for my $key (@keys) {
2809 my @cps = @{$buckets{$key}};
2811 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2813 for my $copyid (@cps) {
2815 next if $seen{$copyid};
2816 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2817 my $copy = $e->retrieve_asset_copy($copyid);
2818 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2820 unless($title) { # grab the title if we don't already have it
2821 my $vol = $e->retrieve_asset_call_number(
2822 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2823 $title = $vol->record;
2826 @status = verify_copy_for_hold(
2827 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2829 $age_protect_only ||= $status[3];
2830 last OUTER if $status[0];
2835 if (!defined($empty_ok)) {
2836 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
2837 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2840 return (1,0) if ($empty_ok);
2842 $status[3] = $age_protect_only;
2846 sub _check_monopart_hold_is_possible {
2847 my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2849 my $e = new_editor();
2850 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2852 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2853 my $copies = $e->json_query(
2855 select => { acp => ['id', 'circ_lib'] },
2859 field => 'target_copy',
2861 filter => { part => $partid }
2865 filter => { holdable => 't', deleted => 'f' },
2868 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
2872 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
2878 $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
2882 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
2883 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2888 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2889 "payload" => {"fail_part" => "no_ultimate_items"}
2897 # -----------------------------------------------------------------------
2898 # sort the copies into buckets based on their circ_lib proximity to
2899 # the patron's home_ou.
2900 # -----------------------------------------------------------------------
2902 my $home_org = $patron->home_ou;
2903 my $req_org = $request_lib->id;
2905 $prox_cache{$home_org} =
2906 $e->search_actor_org_unit_proximity({from_org => $home_org})
2907 unless $prox_cache{$home_org};
2908 my $home_prox = $prox_cache{$home_org};
2909 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2912 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2913 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2915 my @keys = sort { $a <=> $b } keys %buckets;
2918 if( $home_org ne $req_org ) {
2919 # -----------------------------------------------------------------------
2920 # shove the copies close to the request_lib into the primary buckets
2921 # directly before the farthest away copies. That way, they are not
2922 # given priority, but they are checked before the farthest copies.
2923 # -----------------------------------------------------------------------
2924 $prox_cache{$req_org} =
2925 $e->search_actor_org_unit_proximity({from_org => $req_org})
2926 unless $prox_cache{$req_org};
2927 my $req_prox = $prox_cache{$req_org};
2930 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2931 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2933 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2934 my $new_key = $highest_key - 0.5; # right before the farthest prox
2935 my @keys2 = sort { $a <=> $b } keys %buckets2;
2936 for my $key (@keys2) {
2937 last if $key >= $highest_key;
2938 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2942 @keys = sort { $a <=> $b } keys %buckets;
2947 my $age_protect_only = 0;
2948 OUTER: for my $key (@keys) {
2949 my @cps = @{$buckets{$key}};
2951 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2953 for my $copyid (@cps) {
2955 next if $seen{$copyid};
2956 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2957 my $copy = $e->retrieve_asset_copy($copyid);
2958 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2960 unless($title) { # grab the title if we don't already have it
2961 my $vol = $e->retrieve_asset_call_number(
2962 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2963 $title = $vol->record;
2966 @status = verify_copy_for_hold(
2967 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2969 $age_protect_only ||= $status[3];
2970 last OUTER if $status[0];
2975 if (!defined($empty_ok)) {
2976 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
2977 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2980 return (1,0) if ($empty_ok);
2982 $status[3] = $age_protect_only;
2987 sub _check_volume_hold_is_possible {
2988 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2989 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
2990 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
2991 $logger->info("checking possibility of volume hold for volume ".$vol->id);
2993 my $filter_copies = [];
2994 for my $copy (@$copies) {
2995 # ignore part-mapped copies for regular volume level holds
2996 push(@$filter_copies, $copy) unless
2997 new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
2999 $copies = $filter_copies;
3004 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
3005 "payload" => {"fail_part" => "no_ultimate_items"}
3011 my $age_protect_only = 0;
3012 for my $copy ( @$copies ) {
3013 @status = verify_copy_for_hold(
3014 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
3015 $age_protect_only ||= $status[3];
3018 $status[3] = $age_protect_only;
3024 sub verify_copy_for_hold {
3025 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
3026 # $oargs should be undef unless we're overriding.
3027 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
3028 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3031 requestor => $requestor,
3034 title_descriptor => $title->fixed_fields,
3035 pickup_lib => $pickup_lib,
3036 request_lib => $request_lib,
3038 show_event_list => 1
3042 # Check for override permissions on events.
3043 if ($oargs && $permitted && scalar @$permitted) {
3044 # Remove the events from permitted that we can override.
3045 if ($oargs->{events}) {
3046 foreach my $evt (@{$oargs->{events}}) {
3047 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3050 # Now, we handle the override all case by checking remaining
3051 # events against override permissions.
3052 if (scalar @$permitted && $oargs->{all}) {
3053 # Pre-set events and failed members of oargs to empty
3054 # arrays, if they are not set, yet.
3055 $oargs->{events} = [] unless ($oargs->{events});
3056 $oargs->{failed} = [] unless ($oargs->{failed});
3057 # When we're done with these checks, we swap permitted
3058 # with a reference to @disallowed.
3059 my @disallowed = ();
3060 foreach my $evt (@{$permitted}) {
3061 # Check if we've already seen the event in this
3062 # session and it failed.
3063 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3064 push(@disallowed, $evt);
3066 # We have to check if the requestor has the
3067 # override permission.
3069 # AppUtils::check_user_perms returns the perm if
3070 # the user doesn't have it, undef if they do.
3071 if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3072 push(@disallowed, $evt);
3073 push(@{$oargs->{failed}}, $evt->{textcode});
3075 push(@{$oargs->{events}}, $evt->{textcode});
3079 $permitted = \@disallowed;
3083 my $age_protect_only = 0;
3084 if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3085 $age_protect_only = 1;
3089 (not scalar @$permitted), # true if permitted is an empty arrayref
3090 ( # XXX This test is of very dubious value; someone should figure
3091 # out what if anything is checking this value
3092 ($copy->circ_lib == $pickup_lib) and
3093 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3102 sub find_nearest_permitted_hold {
3105 my $editor = shift; # CStoreEditor object
3106 my $copy = shift; # copy to target
3107 my $user = shift; # staff
3108 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3110 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3112 my $bc = $copy->barcode;
3114 # find any existing holds that already target this copy
3115 my $old_holds = $editor->search_action_hold_request(
3116 { current_copy => $copy->id,
3117 cancel_time => undef,
3118 capture_time => undef
3122 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3124 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3125 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3127 my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3129 # the nearest_hold API call now needs this
3130 $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3131 unless ref $copy->call_number;
3133 # search for what should be the best holds for this copy to fulfill
3134 my $best_holds = $U->storagereq(
3135 "open-ils.storage.action.hold_request.nearest_hold.atomic",
3136 $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3138 # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3140 for my $holdid (@$old_holds) {
3141 next unless $holdid;
3142 push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3146 unless(@$best_holds) {
3147 $logger->info("circulator: no suitable holds found for copy $bc");
3148 return (undef, $evt);
3154 # for each potential hold, we have to run the permit script
3155 # to make sure the hold is actually permitted.
3158 for my $holdid (@$best_holds) {
3159 next unless $holdid;
3160 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3162 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3163 # Force and recall holds bypass all rules
3164 if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3168 my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3169 my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3171 $reqr_cache{$hold->requestor} = $reqr;
3172 $org_cache{$hold->request_lib} = $rlib;
3174 # see if this hold is permitted
3175 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3177 patron_id => $hold->usr,
3180 pickup_lib => $hold->pickup_lib,
3181 request_lib => $rlib,
3193 unless( $best_hold ) { # no "good" permitted holds were found
3195 $logger->info("circulator: no suitable holds found for copy $bc");
3196 return (undef, $evt);
3199 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3201 # indicate a permitted hold was found
3202 return $best_hold if $check_only;
3204 # we've found a permitted hold. we need to "grab" the copy
3205 # to prevent re-targeted holds (next part) from re-grabbing the copy
3206 $best_hold->current_copy($copy->id);
3207 $editor->update_action_hold_request($best_hold)
3208 or return (undef, $editor->event);
3213 # re-target any other holds that already target this copy
3214 for my $old_hold (@$old_holds) {
3215 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3216 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3217 $old_hold->id." after a better hold [".$best_hold->id."] was found");
3218 $old_hold->clear_current_copy;
3219 $old_hold->clear_prev_check_time;
3220 $editor->update_action_hold_request($old_hold)
3221 or return (undef, $editor->event);
3222 push(@retarget, $old_hold->id);
3225 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3233 __PACKAGE__->register_method(
3234 method => 'all_rec_holds',
3235 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3239 my( $self, $conn, $auth, $title_id, $args ) = @_;
3241 my $e = new_editor(authtoken=>$auth);
3242 $e->checkauth or return $e->event;
3243 $e->allowed('VIEW_HOLD') or return $e->event;
3246 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
3247 $args->{cancel_time} = undef;
3250 metarecord_holds => []
3252 , volume_holds => []
3254 , recall_holds => []
3257 , issuance_holds => []
3260 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3262 $resp->{metarecord_holds} = $e->search_action_hold_request(
3263 { hold_type => OILS_HOLD_TYPE_METARECORD,
3264 target => $mr_map->metarecord,
3270 $resp->{title_holds} = $e->search_action_hold_request(
3272 hold_type => OILS_HOLD_TYPE_TITLE,
3273 target => $title_id,
3277 my $parts = $e->search_biblio_monograph_part(
3283 $resp->{part_holds} = $e->search_action_hold_request(
3285 hold_type => OILS_HOLD_TYPE_MONOPART,
3291 my $subs = $e->search_serial_subscription(
3292 { record_entry => $title_id }, {idlist=>1});
3295 my $issuances = $e->search_serial_issuance(
3296 {subscription => $subs}, {idlist=>1}
3300 $resp->{issuance_holds} = $e->search_action_hold_request(
3302 hold_type => OILS_HOLD_TYPE_ISSUANCE,
3303 target => $issuances,
3310 my $vols = $e->search_asset_call_number(
3311 { record => $title_id, deleted => 'f' }, {idlist=>1});
3313 return $resp unless @$vols;
3315 $resp->{volume_holds} = $e->search_action_hold_request(
3317 hold_type => OILS_HOLD_TYPE_VOLUME,
3322 my $copies = $e->search_asset_copy(
3323 { call_number => $vols, deleted => 'f' }, {idlist=>1});
3325 return $resp unless @$copies;
3327 $resp->{copy_holds} = $e->search_action_hold_request(
3329 hold_type => OILS_HOLD_TYPE_COPY,
3334 $resp->{recall_holds} = $e->search_action_hold_request(
3336 hold_type => OILS_HOLD_TYPE_RECALL,
3341 $resp->{force_holds} = $e->search_action_hold_request(
3343 hold_type => OILS_HOLD_TYPE_FORCE,
3351 __PACKAGE__->register_method(
3352 method => 'stream_wide_holds',
3355 api_name => 'open-ils.circ.hold.wide_hash.stream'
3358 sub stream_wide_holds {
3359 my($self, $client, $auth, $restrictions, $order_by, $limit, $offset) = @_;
3361 my $e = new_editor(authtoken=>$auth);
3362 $e->checkauth or return $e->event;
3363 $e->allowed('VIEW_HOLD') or return $e->event;
3365 my $st = OpenSRF::AppSession->create('open-ils.storage');
3366 my $req = $st->request(
3367 'open-ils.storage.action.live_holds.wide_hash',
3368 $restrictions, $order_by, $limit, $offset
3371 my $count = $req->recv;
3376 if(UNIVERSAL::isa($count,"Error")) {
3377 throw $count ($count->stringify);
3380 $count = $count->content;
3382 # Force immediate send of count response
3383 my $mbc = $client->max_bundle_count;
3384 $client->max_bundle_count(1);
3385 $client->respond($count);
3386 $client->max_bundle_count($mbc);
3388 while (my $hold = $req->recv) {
3389 $client->respond($hold->content) if $hold->content;
3392 $client->respond_complete;
3398 __PACKAGE__->register_method(
3399 method => 'uber_hold',
3401 api_name => 'open-ils.circ.hold.details.retrieve'
3405 my($self, $client, $auth, $hold_id, $args) = @_;
3406 my $e = new_editor(authtoken=>$auth);
3407 $e->checkauth or return $e->event;
3408 return uber_hold_impl($e, $hold_id, $args);
3411 __PACKAGE__->register_method(
3412 method => 'batch_uber_hold',
3415 api_name => 'open-ils.circ.hold.details.batch.retrieve'
3418 sub batch_uber_hold {
3419 my($self, $client, $auth, $hold_ids, $args) = @_;
3420 my $e = new_editor(authtoken=>$auth);
3421 $e->checkauth or return $e->event;
3422 $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3426 sub uber_hold_impl {
3427 my($e, $hold_id, $args) = @_;
3430 my $flesh_fields = ['current_copy', 'usr', 'notes'];
3431 push (@$flesh_fields, 'requestor') if $args->{include_requestor};
3432 push (@$flesh_fields, 'cancel_cause') if $args->{include_cancel_cause};
3434 my $hold = $e->retrieve_action_hold_request([
3436 {flesh => 1, flesh_fields => {ahr => $flesh_fields}}
3437 ]) or return $e->event;
3439 if($hold->usr->id ne $e->requestor->id) {
3440 # caller is asking for someone else's hold
3441 $e->allowed('VIEW_HOLD') or return $e->event;
3442 $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3443 [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3446 # caller is asking for own hold, but may not have permission to view staff notes
3447 unless($e->allowed('VIEW_HOLD')) {
3448 $hold->notes( # filter out any staff notes (unless marked as public)
3449 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3453 my $user = $hold->usr;
3454 $hold->usr($user->id);
3457 my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args);
3459 flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3460 flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3462 my $details = retrieve_hold_queue_status_impl($e, $hold);
3463 $hold->usr($user) if $args->{include_usr}; # re-flesh
3468 ($copy ? (copy => $copy) : ()),
3469 ($volume ? (volume => $volume) : ()),
3470 ($issuance ? (issuance => $issuance) : ()),
3471 ($part ? (part => $part) : ()),
3472 ($args->{include_bre} ? (bre => $bre) : ()),
3473 ($args->{suppress_mvr} ? () : (mvr => $mvr)),
3477 $resp->{copy}->location(
3478 $e->retrieve_asset_copy_location($resp->{copy}->location))
3479 if $resp->{copy} and $args->{flesh_acpl};
3481 unless($args->{suppress_patron_details}) {
3482 my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3483 $resp->{patron_first} = $user->first_given_name,
3484 $resp->{patron_last} = $user->family_name,
3485 $resp->{patron_barcode} = $card->barcode,
3486 $resp->{patron_alias} = $user->alias,
3494 # -----------------------------------------------------
3495 # Returns the MVR object that represents what the
3497 # -----------------------------------------------------
3499 my( $e, $hold, $args ) = @_;
3507 my $no_mvr = $args->{suppress_mvr};
3509 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3510 $metarecord = $e->retrieve_metabib_metarecord($hold->target)
3511 or return $e->event;
3512 $tid = $metarecord->master_record;
3514 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3515 $tid = $hold->target;
3517 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3518 $volume = $e->retrieve_asset_call_number($hold->target)
3519 or return $e->event;
3520 $tid = $volume->record;
3522 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3523 $issuance = $e->retrieve_serial_issuance([
3525 {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3526 ]) or return $e->event;
3528 $tid = $issuance->subscription->record_entry;
3530 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3531 $part = $e->retrieve_biblio_monograph_part([
3533 ]) or return $e->event;
3535 $tid = $part->record;
3537 } 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 ) {
3538 $copy = $e->retrieve_asset_copy([
3540 {flesh => 1, flesh_fields => {acp => ['call_number']}}
3541 ]) or return $e->event;
3543 $volume = $copy->call_number;
3544 $tid = $volume->record;
3547 if(!$copy and ref $hold->current_copy ) {
3548 $copy = $hold->current_copy;
3549 $hold->current_copy($copy->id) unless $args->{include_current_copy};
3552 if(!$volume and $copy) {
3553 $volume = $e->retrieve_asset_call_number($copy->call_number);
3556 # TODO return metarcord mvr for M holds
3557 my $title = $e->retrieve_biblio_record_entry($tid);
3558 return ( ($no_mvr) ? undef : $U->record_to_mvr($title),
3559 $volume, $copy, $issuance, $part, $title, $metarecord);
3562 __PACKAGE__->register_method(
3563 method => 'clear_shelf_cache',
3564 api_name => 'open-ils.circ.hold.clear_shelf.get_cache',
3568 Returns the holds processed with the given cache key
3573 sub clear_shelf_cache {
3574 my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3575 my $e = new_editor(authtoken => $auth, xact => 1);
3576 return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3579 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3581 my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3584 $logger->info("no hold data found in cache"); # XXX TODO return event
3590 foreach (keys %$hold_data) {
3591 $maximum += scalar(@{ $hold_data->{$_} });
3593 $client->respond({"maximum" => $maximum, "progress" => 0});
3595 for my $action (sort keys %$hold_data) {
3596 while (@{$hold_data->{$action}}) {
3597 my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3599 my $result_chunk = $e->json_query({
3601 "acp" => ["barcode"],
3603 first_given_name second_given_name family_name alias
3606 "acnp" => [{column => "label", alias => "prefix"}],
3607 "acns" => [{column => "label", alias => "suffix"}],
3615 "field" => "id", "fkey" => "current_copy",
3618 "field" => "id", "fkey" => "call_number",
3621 "field" => "id", "fkey" => "record"
3624 "field" => "id", "fkey" => "prefix"
3627 "field" => "id", "fkey" => "suffix"
3631 "acpl" => {"field" => "id", "fkey" => "location"}
3634 "au" => {"field" => "id", "fkey" => "usr"}
3637 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3638 }, {"substream" => 1}) or return $e->die_event;
3642 +{"action" => $action, "hold_details" => $_}
3653 __PACKAGE__->register_method(
3654 method => 'clear_shelf_process',
3656 api_name => 'open-ils.circ.hold.clear_shelf.process',
3659 1. Find all holds that have expired on the holds shelf
3661 3. If a clear-shelf status is configured, put targeted copies into this status
3662 4. Divide copies into 3 groups: items to transit, items to reshelve, and items
3663 that are needed for holds. No subsequent action is taken on the holds
3664 or items after grouping.
3669 sub clear_shelf_process {
3670 my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
3672 my $e = new_editor(authtoken=>$auth);
3673 $e->checkauth or return $e->die_event;
3674 my $cache = OpenSRF::Utils::Cache->new('global');
3676 $org_id ||= $e->requestor->ws_ou;
3677 $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
3679 my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
3681 my @hold_ids = $self->method_lookup(
3682 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
3683 )->run($auth, $org_id, $match_copy);
3688 my @canceled_holds; # newly canceled holds
3689 $chunk_size ||= 25; # chunked status updates
3690 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3693 for my $hold_id (@hold_ids) {
3695 $logger->info("Clear shelf processing hold $hold_id");
3697 my $hold = $e->retrieve_action_hold_request([
3700 flesh_fields => {ahr => ['current_copy']}
3704 if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
3705 $hold->cancel_time('now');
3706 $hold->cancel_cause(2); # Hold Shelf expiration
3707 $e->update_action_hold_request($hold) or return $e->die_event;
3708 push(@canceled_holds, $hold_id);
3711 my $copy = $hold->current_copy;
3713 if($copy_status or $copy_status == 0) {
3714 # if a clear-shelf copy status is defined, update the copy
3715 $copy->status($copy_status);
3716 $copy->edit_date('now');
3717 $copy->editor($e->requestor->id);
3718 $e->update_asset_copy($copy) or return $e->die_event;
3721 push(@holds, $hold);
3722 $client->respond({maximum => scalar(@holds), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
3731 pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
3734 for my $hold (@holds) {
3736 my $copy = $hold->current_copy;
3737 my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
3739 if($alt_hold and !$match_copy) {
3741 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
3743 } elsif($copy->circ_lib != $e->requestor->ws_ou) {
3745 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
3749 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
3753 my $cache_key = md5_hex(time . $$ . rand());
3754 $logger->info("clear_shelf_cache: storing under $cache_key");
3755 $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours. configurable?
3757 # tell the client we're done
3758 $client->respond_complete({cache_key => $cache_key});
3761 # fire off the hold cancelation trigger and wait for response so don't flood the service
3763 # refetch the holds to pick up the caclulated cancel_time,
3764 # which may be needed by Action/Trigger
3766 my $updated_holds = [];
3767 $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
3770 $U->create_events_for_hook(
3771 'hold_request.cancel.expire_holds_shelf',
3772 $_, $org_id, undef, undef, 1) for @$updated_holds;
3775 # tell the client we're done
3776 $client->respond_complete;
3780 # returns IDs for holds that are on the holds shelf but
3781 # have had their pickup_libs change while on the shelf.
3782 sub pickup_lib_changed_on_shelf_holds {
3785 my $ignore_holds = shift;
3786 $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
3789 select => { alhr => ['id'] },
3794 fkey => 'current_copy'
3799 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
3801 capture_time => { "!=" => undef },
3802 fulfillment_time => undef,
3803 current_shelf_lib => $org_id,
3804 pickup_lib => {'!=' => {'+alhr' => 'current_shelf_lib'}}
3809 $query->{where}->{'+alhr'}->{id} =
3810 {'not in' => $ignore_holds} if @$ignore_holds;
3812 my $hold_ids = $e->json_query($query);
3813 return [ map { $_->{id} } @$hold_ids ];
3816 __PACKAGE__->register_method(
3817 method => 'usr_hold_summary',
3818 api_name => 'open-ils.circ.holds.user_summary',
3820 Returns a summary of holds statuses for a given user
3824 sub usr_hold_summary {
3825 my($self, $conn, $auth, $user_id) = @_;
3827 my $e = new_editor(authtoken=>$auth);
3828 $e->checkauth or return $e->event;
3829 $e->allowed('VIEW_HOLD') or return $e->event;
3831 my $holds = $e->search_action_hold_request(
3834 fulfillment_time => undef,
3835 cancel_time => undef,
3839 my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
3840 $summary{_hold_status($e, $_)} += 1 for @$holds;
3846 __PACKAGE__->register_method(
3847 method => 'hold_has_copy_at',
3848 api_name => 'open-ils.circ.hold.has_copy_at',
3851 'Returns the ID of the found copy and name of the shelving location if there is ' .
3852 'an available copy at the specified org unit. Returns empty hash otherwise. ' .
3853 'The anticipated use for this method is to determine whether an item is ' .
3854 'available at the library where the user is placing the hold (or, alternatively, '.
3855 'at the pickup library) to encourage bypassing the hold placement and just ' .
3856 'checking out the item.' ,
3858 { desc => 'Authentication Token', type => 'string' },
3859 { desc => 'Method Arguments. Options include: hold_type, hold_target, org_unit. '
3860 . 'hold_type is the hold type code (T, V, C, M, ...). '
3861 . 'hold_target is the identifier of the hold target object. '
3862 . 'org_unit is org unit ID.',
3867 desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
3873 sub hold_has_copy_at {
3874 my($self, $conn, $auth, $args) = @_;
3876 my $e = new_editor(authtoken=>$auth);
3877 $e->checkauth or return $e->event;
3879 my $hold_type = $$args{hold_type};
3880 my $hold_target = $$args{hold_target};
3881 my $org_unit = $$args{org_unit};
3884 select => {acp => ['id'], acpl => ['name']},
3889 filter => { holdable => 't', deleted => 'f' },
3892 ccs => {field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3895 where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit, status => [0,7]}},
3899 if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
3901 $query->{where}->{'+acp'}->{id} = $hold_target;
3903 } elsif($hold_type eq 'V') {
3905 $query->{where}->{'+acp'}->{call_number} = $hold_target;
3907 } elsif($hold_type eq 'P') {
3909 $query->{from}->{acp}->{acpm} = {
3910 field => 'target_copy',
3912 filter => {part => $hold_target},
3915 } elsif($hold_type eq 'I') {
3917 $query->{from}->{acp}->{sitem} = {
3920 filter => {issuance => $hold_target},
3923 } elsif($hold_type eq 'T') {
3925 $query->{from}->{acp}->{acn} = {
3927 fkey => 'call_number',
3931 filter => {id => $hold_target},
3939 $query->{from}->{acp}->{acn} = {
3941 fkey => 'call_number',
3950 filter => {metarecord => $hold_target},
3958 my $res = $e->json_query($query)->[0] or return {};
3959 return {copy => $res->{id}, location => $res->{name}} if $res;
3963 # returns true if the user already has an item checked out
3964 # that could be used to fulfill the requested hold.
3965 sub hold_item_is_checked_out {
3966 my($e, $user_id, $hold_type, $hold_target) = @_;
3969 select => {acp => ['id']},
3970 from => {acp => {}},
3974 in => { # copies for circs the user has checked out
3975 select => {circ => ['target_copy']},
3979 checkin_time => undef,
3981 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
3982 {stop_fines => undef}
3992 if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
3994 $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
3996 } elsif($hold_type eq 'V') {
3998 $query->{where}->{'+acp'}->{call_number} = $hold_target;
4000 } elsif($hold_type eq 'P') {
4002 $query->{from}->{acp}->{acpm} = {
4003 field => 'target_copy',
4005 filter => {part => $hold_target},
4008 } elsif($hold_type eq 'I') {
4010 $query->{from}->{acp}->{sitem} = {
4013 filter => {issuance => $hold_target},
4016 } elsif($hold_type eq 'T') {
4018 $query->{from}->{acp}->{acn} = {
4020 fkey => 'call_number',
4024 filter => {id => $hold_target},
4032 $query->{from}->{acp}->{acn} = {
4034 fkey => 'call_number',
4043 filter => {metarecord => $hold_target},
4051 return $e->json_query($query)->[0];
4054 __PACKAGE__->register_method(
4055 method => 'change_hold_title',
4056 api_name => 'open-ils.circ.hold.change_title',
4059 Updates all title level holds targeting the specified bibs to point a new bib./,
4061 { desc => 'Authentication Token', type => 'string' },
4062 { desc => 'New Target Bib Id', type => 'number' },
4063 { desc => 'Old Target Bib Ids', type => 'array' },
4065 return => { desc => '1 on success' }
4069 __PACKAGE__->register_method(
4070 method => 'change_hold_title_for_specific_holds',
4071 api_name => 'open-ils.circ.hold.change_title.specific_holds',
4074 Updates specified holds to target new bib./,
4076 { desc => 'Authentication Token', type => 'string' },
4077 { desc => 'New Target Bib Id', type => 'number' },
4078 { desc => 'Holds Ids for holds to update', type => 'array' },
4080 return => { desc => '1 on success' }
4085 sub change_hold_title {
4086 my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4088 my $e = new_editor(authtoken=>$auth, xact=>1);
4089 return $e->die_event unless $e->checkauth;
4091 my $holds = $e->search_action_hold_request(
4094 capture_time => undef,
4095 cancel_time => undef,
4096 fulfillment_time => undef,
4102 flesh_fields => { ahr => ['usr'] }
4108 for my $hold (@$holds) {
4109 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4110 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4111 $hold->target( $new_bib_id );
4112 $e->update_action_hold_request($hold) or return $e->die_event;
4117 _reset_hold($self, $e->requestor, $_) for @$holds;
4122 sub change_hold_title_for_specific_holds {
4123 my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4125 my $e = new_editor(authtoken=>$auth, xact=>1);
4126 return $e->die_event unless $e->checkauth;
4128 my $holds = $e->search_action_hold_request(
4131 capture_time => undef,
4132 cancel_time => undef,
4133 fulfillment_time => undef,
4139 flesh_fields => { ahr => ['usr'] }
4145 for my $hold (@$holds) {
4146 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4147 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4148 $hold->target( $new_bib_id );
4149 $e->update_action_hold_request($hold) or return $e->die_event;
4154 _reset_hold($self, $e->requestor, $_) for @$holds;
4159 __PACKAGE__->register_method(
4160 method => 'rec_hold_count',
4161 api_name => 'open-ils.circ.bre.holds.count',
4163 desc => q/Returns the total number of holds that target the
4164 selected bib record or its associated copies and call_numbers/,
4166 { desc => 'Bib ID', type => 'number' },
4167 { desc => q/Optional arguments. Supported arguments include:
4168 "pickup_lib_descendant" -> limit holds to those whose pickup
4169 library is equal to or is a child of the provided org unit/,
4173 return => {desc => 'Hold count', type => 'number'}
4177 __PACKAGE__->register_method(
4178 method => 'rec_hold_count',
4179 api_name => 'open-ils.circ.mmr.holds.count',
4181 desc => q/Returns the total number of holds that target the
4182 selected metarecord or its associated copies, call_numbers, and bib records/,
4184 { desc => 'Metarecord ID', type => 'number' },
4186 return => {desc => 'Hold count', type => 'number'}
4190 # XXX Need to add type I holds to these counts
4191 sub rec_hold_count {
4192 my($self, $conn, $target_id, $args) = @_;
4199 filter => {metarecord => $target_id}
4206 filter => { id => $target_id },
4211 if($self->api_name =~ /mmr/) {
4212 delete $bre_join->{bre}->{filter};
4213 $bre_join->{bre}->{join} = $mmr_join;
4219 fkey => 'call_number',
4225 select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4229 cancel_time => undef,
4230 fulfillment_time => undef,
4234 hold_type => [qw/C F R/],
4237 select => {acp => ['id']},
4238 from => { acp => $cn_join }
4248 select => {acn => ['id']},
4249 from => {acn => $bre_join}
4259 select => {bmp => ['id']},
4260 from => {bmp => $bre_join}
4268 target => $target_id
4276 if($self->api_name =~ /mmr/) {
4277 $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4282 select => {bre => ['id']},
4283 from => {bre => $mmr_join}
4289 $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4292 target => $target_id
4298 if (my $pld = $args->{pickup_lib_descendant}) {
4300 my $top_ou = new_editor()->search_actor_org_unit(
4301 {parent_ou => undef}
4302 )->[0]; # XXX Assumes single root node. Not alone in this...
4304 $query->{where}->{'+ahr'}->{pickup_lib} = {
4306 select => {aou => [{
4308 transform => 'actor.org_unit_descendants',
4309 result_field => 'id'
4312 where => {id => $pld}
4314 } if ($pld != $top_ou->id);
4318 return new_editor()->json_query($query)->[0]->{count};
4321 # A helper function to calculate a hold's expiration time at a given
4322 # org_unit. Takes the org_unit as an argument and returns either the
4323 # hold expire time as an ISO8601 string or undef if there is no hold
4324 # expiration interval set for the subject ou.
4325 sub calculate_expire_time
4328 my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4330 my $date = DateTime->now->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($interval));
4331 return $U->epoch2ISO8601($date->epoch);
4337 __PACKAGE__->register_method(
4338 method => 'mr_hold_filter_attrs',
4339 api_name => 'open-ils.circ.mmr.holds.filters',
4344 Returns the set of available formats and languages for the
4345 constituent records of the provided metarcord.
4346 If an array of hold IDs is also provided, information about
4347 each is returned as well. This information includes:
4348 1. a slightly easier to read version of holdable_formats
4349 2. attributes describing the set of format icons included
4350 in the set of desired, constituent records.
4353 {desc => 'Metarecord ID', type => 'number'},
4354 {desc => 'Context Org ID', type => 'number'},
4355 {desc => 'Hold ID List', type => 'array'},
4359 Stream of objects. The first will have a 'metarecord' key
4360 containing non-hold-specific metarecord information, subsequent
4361 responses will contain a 'hold' key containing hold-specific
4369 sub mr_hold_filter_attrs {
4370 my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4371 my $e = new_editor();
4373 # by default, return MR / hold attributes for all constituent
4374 # records with holdable copies. If there is a hard boundary,
4375 # though, limit to records with copies within the boundary,
4376 # since anything outside the boundary can never be held.
4379 $org_depth = $U->ou_ancestor_setting_value(
4380 $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4383 # get all org-scoped records w/ holdable copies for this metarecord
4384 my ($bre_ids) = $self->method_lookup(
4385 'open-ils.circ.holds.metarecord.filtered_records')->run(
4386 $mr_id, undef, $org_id, $org_depth);
4388 my $item_lang_attr = 'item_lang'; # configurable?
4389 my $format_attr = $e->retrieve_config_global_flag(
4390 'opac.metarecord.holds.format_attr')->value;
4392 # helper sub for fetching ccvms for a batch of record IDs
4393 sub get_batch_ccvms {
4394 my ($e, $attr, $bre_ids) = @_;
4395 return [] unless $bre_ids and @$bre_ids;
4396 my $vals = $e->search_metabib_record_attr_flat({
4400 return [] unless @$vals;
4401 return $e->search_config_coded_value_map({
4403 code => [map {$_->value} @$vals]
4407 my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4408 my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4413 formats => $formats,
4418 return unless $hold_ids;
4419 my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4420 $icon_attr = $icon_attr ? $icon_attr->value : '';
4422 for my $hold_id (@$hold_ids) {
4423 my $hold = $e->retrieve_action_hold_request($hold_id)
4424 or return $e->event;
4426 next unless $hold->hold_type eq 'M';
4436 # collect the ccvm's for the selected formats / language
4437 # (i.e. the holdable formats) on the MR.
4438 # this assumes a two-key structure for format / language,
4439 # though no assumption is made about the keys themselves.
4440 my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4442 my $format_vals = [];
4443 for my $val (values %$hformats) {
4444 # val is either a single ccvm or an array of them
4445 $val = [$val] unless ref $val eq 'ARRAY';
4446 for my $node (@$val) {
4447 push (@$lang_vals, $node->{_val})
4448 if $node->{_attr} eq $item_lang_attr;
4449 push (@$format_vals, $node->{_val})
4450 if $node->{_attr} eq $format_attr;
4454 # fetch the ccvm's for consistency with the {metarecord} blob
4455 $resp->{hold}{formats} = $e->search_config_coded_value_map({
4456 ctype => $format_attr, code => $format_vals});
4457 $resp->{hold}{langs} = $e->search_config_coded_value_map({
4458 ctype => $item_lang_attr, code => $lang_vals});
4460 # find all of the bib records within this metarcord whose
4461 # format / language match the holdable formats on the hold
4462 my ($bre_ids) = $self->method_lookup(
4463 'open-ils.circ.holds.metarecord.filtered_records')->run(
4464 $hold->target, $hold->holdable_formats,
4465 $hold->selection_ou, $hold->selection_depth);
4467 # now find all of the 'icon' attributes for the records
4468 $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4469 $client->respond($resp);
4475 __PACKAGE__->register_method(
4476 method => "copy_has_holds_count",
4477 api_name => "open-ils.circ.copy.has_holds_count",
4481 Returns the number of holds a paticular copy has
4484 { desc => 'Authentication Token', type => 'string'},
4485 { desc => 'Copy ID', type => 'number'}
4496 sub copy_has_holds_count {
4497 my( $self, $conn, $auth, $copyid ) = @_;
4498 my $e = new_editor(authtoken=>$auth);
4499 return $e->event unless $e->checkauth;
4501 if( $copyid && $copyid > 0 ) {
4502 my $meth = 'retrieve_action_has_holds_count';
4503 my $data = $e->$meth($copyid);
4505 return $data->count();
4511 __PACKAGE__->register_method(
4512 method => "hold_metadata",
4513 api_name => "open-ils.circ.hold.get_metadata",
4518 Returns a stream of objects containing whatever bib,
4519 volume, etc. data is available to the specific hold
4523 {desc => 'Hold Type', type => 'string'},
4524 {desc => 'Hold Target(s)', type => 'number or array'},
4525 {desc => 'Context org unit (optional)', type => 'number'}
4529 Stream of hold metadata objects.
4537 my ($self, $client, $hold_type, $hold_targets, $org_id) = @_;
4539 $hold_targets = [$hold_targets] unless ref $hold_targets;
4541 my $e = new_editor();
4542 for my $target (@$hold_targets) {
4544 # create a dummy hold for find_hold_mvr
4545 my $hold = Fieldmapper::action::hold_request->new;
4546 $hold->hold_type($hold_type);
4547 $hold->target($target);
4549 my (undef, $volume, $copy, $issuance, $part, $bre, $metarecord) =
4550 find_hold_mvr($e, $hold, {suppress_mvr => 1});
4552 $bre->clear_marc; # avoid bulk
4558 issuance => $issuance,
4561 metarecord => $metarecord,
4562 metarecord_filters => {}
4565 # If this is a bib hold or metarecord hold, also return the
4566 # available set of MR filters (AKA "Holdable Formats") for the
4567 # hold. For bib holds these may be used to upgrade the hold
4568 # from a bib to metarecord hold.
4569 if ($hold_type eq 'T') {
4570 my $map = $e->search_metabib_metarecord_source_map(
4571 {source => $meta->{bibrecord}->id})->[0];
4574 $meta->{metarecord} =
4575 $e->retrieve_metabib_metarecord($map->metarecord);
4579 if ($meta->{metarecord}) {
4582 $self->method_lookup('open-ils.circ.mmr.holds.filters')
4583 ->run($meta->{metarecord}->id, $org_id);
4586 $meta->{metarecord_filters} = $filters->{metarecord};
4590 $client->respond($meta);