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 OpenSRF::Utils 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 cleanse_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);
297 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
299 my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
300 push( @events, OpenILS::Event->new('HOLD_ITEM_CHECKED_OUT')) if $checked_out;
302 if ( $t eq OILS_HOLD_TYPE_METARECORD ) {
303 return $e->die_event unless $e->allowed('MR_HOLDS', $porg);
304 } elsif ( $t eq OILS_HOLD_TYPE_TITLE ) {
305 return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
306 } elsif ( $t eq OILS_HOLD_TYPE_VOLUME ) {
307 return $e->die_event unless $e->allowed('VOLUME_HOLDS', $porg);
308 } elsif ( $t eq OILS_HOLD_TYPE_MONOPART ) {
309 return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
310 } elsif ( $t eq OILS_HOLD_TYPE_ISSUANCE ) {
311 return $e->die_event unless $e->allowed('ISSUANCE_HOLDS', $porg);
312 } elsif ( $t eq OILS_HOLD_TYPE_COPY ) {
313 return $e->die_event unless $e->allowed('COPY_HOLDS', $porg);
314 } elsif ( $t eq OILS_HOLD_TYPE_FORCE || $t eq OILS_HOLD_TYPE_RECALL ) {
315 my $copy = $e->retrieve_asset_copy($hold->target)
316 or return $e->die_event;
317 if ( $t eq OILS_HOLD_TYPE_FORCE ) {
318 return $e->die_event unless $e->allowed('COPY_HOLDS_FORCE', $copy->circ_lib);
319 } elsif ( $t eq OILS_HOLD_TYPE_RECALL ) {
320 return $e->die_event unless $e->allowed('COPY_HOLDS_RECALL', $copy->circ_lib);
329 for my $evt (@events) {
331 my $name = $evt->{textcode};
332 if($oargs->{all} || grep { $_ eq $name } @{$oargs->{events}}) {
333 return $e->die_event unless $e->allowed("$name.override", $porg);
341 # Check for hold expiration in the past, and set it to empty string.
342 $hold->expire_time(undef) if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1);
344 # set the configured expire time
345 unless($hold->expire_time) {
346 $hold->expire_time(calculate_expire_time($recipient->home_ou));
350 # if behind-the-desk pickup is supported at the hold pickup lib,
351 # set the value to the patron default, unless a value has already
352 # been applied. If it's not supported, force the value to false.
354 my $bdous = $U->ou_ancestor_setting_value(
356 'circ.holds.behind_desk_pickup_supported', $e);
359 if (!defined $hold->behind_desk) {
361 my $set = $e->search_actor_user_setting({
363 name => 'circ.holds_behind_desk'
366 $hold->behind_desk('t') if $set and
367 OpenSRF::Utils::JSON->JSON2perl($set->value);
370 # behind the desk not supported, force it to false
371 $hold->behind_desk('f');
374 $hold->requestor($e->requestor->id);
375 $hold->request_lib($e->requestor->ws_ou);
376 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
377 $hold = $e->create_action_hold_request($hold) or return $e->die_event;
381 $conn->respond_complete($hold->id);
383 $U->simplereq('open-ils.hold-targeter',
384 'open-ils.hold-targeter.target', {hold => $hold->id}
385 ) unless $U->is_true($hold->frozen);
390 # makes sure that a user has permission to place the type of requested hold
391 # returns the Perm exception if not allowed, returns undef if all is well
392 sub _check_holds_perm {
393 my($type, $user_id, $org_id) = @_;
397 $evt = $apputils->check_perms($user_id, $org_id, "MR_HOLDS" );
398 } elsif ($type eq "T") {
399 $evt = $apputils->check_perms($user_id, $org_id, "TITLE_HOLDS" );
400 } elsif($type eq "V") {
401 $evt = $apputils->check_perms($user_id, $org_id, "VOLUME_HOLDS");
402 } elsif($type eq "C") {
403 $evt = $apputils->check_perms($user_id, $org_id, "COPY_HOLDS" );
410 # tests if the given user is allowed to place holds on another's behalf
411 sub _check_request_holds_perm {
414 if (my $evt = $apputils->check_perms(
415 $user_id, $org_id, "REQUEST_HOLDS")) {
420 my $ses_is_req_note = 'The login session is the requestor. If the requestor is different from the user, ' .
421 'then the requestor must have VIEW_HOLD permissions';
423 __PACKAGE__->register_method(
424 method => "retrieve_holds_by_id",
425 api_name => "open-ils.circ.holds.retrieve_by_id",
427 desc => "Retrieve the hold, with hold transits attached, for the specified ID. $ses_is_req_note",
429 { desc => 'Authentication token', type => 'string' },
430 { desc => 'Hold ID', type => 'number' }
433 desc => 'Hold object with transits attached, event on error',
439 sub retrieve_holds_by_id {
440 my($self, $client, $auth, $hold_id) = @_;
441 my $e = new_editor(authtoken=>$auth);
442 $e->checkauth or return $e->event;
443 $e->allowed('VIEW_HOLD') or return $e->event;
445 my $holds = $e->search_action_hold_request(
447 { id => $hold_id , fulfillment_time => undef },
449 order_by => { ahr => "request_time" },
451 flesh_fields => {ahr => ['notes']}
456 flesh_hold_transits($holds);
457 flesh_hold_notices($holds, $e);
462 __PACKAGE__->register_method(
463 method => "retrieve_holds",
464 api_name => "open-ils.circ.holds.retrieve",
466 desc => "Retrieves all the holds, with hold transits attached, for the specified user. $ses_is_req_note",
468 { desc => 'Authentication token', type => 'string' },
469 { desc => 'User ID', type => 'integer' },
470 { desc => 'Available Only', type => 'boolean' }
473 desc => 'list of holds, event on error',
478 __PACKAGE__->register_method(
479 method => "retrieve_holds",
480 api_name => "open-ils.circ.holds.id_list.retrieve",
483 desc => "Retrieves all the hold IDs, for the specified user. $ses_is_req_note",
485 { desc => 'Authentication token', type => 'string' },
486 { desc => 'User ID', type => 'integer' },
487 { desc => 'Available Only', type => 'boolean' }
490 desc => 'list of holds, event on error',
495 __PACKAGE__->register_method(
496 method => "retrieve_holds",
497 api_name => "open-ils.circ.holds.canceled.retrieve",
500 desc => "Retrieves all the cancelled holds for the specified user. $ses_is_req_note",
502 { desc => 'Authentication token', type => 'string' },
503 { desc => 'User ID', type => 'integer' }
506 desc => 'list of holds, event on error',
511 __PACKAGE__->register_method(
512 method => "retrieve_holds",
513 api_name => "open-ils.circ.holds.canceled.id_list.retrieve",
516 desc => "Retrieves list of cancelled hold IDs for the specified user. $ses_is_req_note",
518 { desc => 'Authentication token', type => 'string' },
519 { desc => 'User ID', type => 'integer' }
522 desc => 'list of hold IDs, event on error',
529 my ($self, $client, $auth, $user_id, $available) = @_;
531 my $e = new_editor(authtoken=>$auth);
532 return $e->event unless $e->checkauth;
533 $user_id = $e->requestor->id unless defined $user_id;
535 my $notes_filter = {staff => 'f'};
536 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
537 unless($user_id == $e->requestor->id) {
538 if($e->allowed('VIEW_HOLD', $user->home_ou)) {
539 $notes_filter = {staff => 't'}
541 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
542 $e, $user_id, $e->requestor->id, 'hold.view');
543 return $e->event unless $allowed;
546 # staff member looking at his/her own holds can see staff and non-staff notes
547 $notes_filter = {} if $e->allowed('VIEW_HOLD', $user->home_ou);
551 select => {ahr => ['id']},
553 where => {usr => $user_id, fulfillment_time => undef}
556 if($self->api_name =~ /canceled/) {
558 # Fetch the canceled holds
559 # order cancelled holds by cancel time, most recent first
561 $holds_query->{order_by} = [{class => 'ahr', field => 'cancel_time', direction => 'desc'}];
564 my $cancel_count = $U->ou_ancestor_setting_value(
565 $e->requestor->ws_ou, 'circ.holds.canceled.display_count', $e);
567 unless($cancel_count) {
568 $cancel_age = $U->ou_ancestor_setting_value(
569 $e->requestor->ws_ou, 'circ.holds.canceled.display_age', $e);
571 # if no settings are defined, default to last 10 cancelled holds
572 $cancel_count = 10 unless $cancel_age;
575 if($cancel_count) { # limit by count
577 $holds_query->{where}->{cancel_time} = {'!=' => undef};
578 $holds_query->{limit} = $cancel_count;
580 } elsif($cancel_age) { # limit by age
582 # find all of the canceled holds that were canceled within the configured time frame
583 my $date = DateTime->now->subtract(seconds => OpenSRF::Utils::interval_to_seconds($cancel_age));
584 $date = $U->epoch2ISO8601($date->epoch);
585 $holds_query->{where}->{cancel_time} = {'>=' => $date};
590 # order non-cancelled holds by ready-for-pickup, then active, followed by suspended
591 # "compare" sorts false values to the front. testing pickup_lib != current_shelf_lib
592 # will sort by pl = csl > pl != csl > followed by csl is null;
593 $holds_query->{order_by} = [
595 field => 'pickup_lib',
596 compare => {'!=' => {'+ahr' => 'current_shelf_lib'}}},
597 {class => 'ahr', field => 'shelf_time'},
598 {class => 'ahr', field => 'frozen'},
599 {class => 'ahr', field => 'request_time'}
602 $holds_query->{where}->{cancel_time} = undef;
604 $holds_query->{where}->{shelf_time} = {'!=' => undef};
606 $holds_query->{where}->{pickup_lib} = {'=' => {'+ahr' => 'current_shelf_lib'}};
610 my $hold_ids = $e->json_query($holds_query);
611 $hold_ids = [ map { $_->{id} } @$hold_ids ];
613 return $hold_ids if $self->api_name =~ /id_list/;
616 for my $hold_id ( @$hold_ids ) {
618 my $hold = $e->retrieve_action_hold_request($hold_id);
619 $hold->notes($e->search_action_hold_request_note({hold => $hold_id, %$notes_filter}));
622 $e->search_action_hold_transit_copy([
623 {hold => $hold->id, cancel_time => undef},
624 {order_by => {ahtc => 'source_send_time desc'}, limit => 1}])->[0]
634 __PACKAGE__->register_method(
635 method => 'user_hold_count',
636 api_name => 'open-ils.circ.hold.user.count'
639 sub user_hold_count {
640 my ( $self, $conn, $auth, $userid ) = @_;
641 my $e = new_editor( authtoken => $auth );
642 return $e->event unless $e->checkauth;
643 my $patron = $e->retrieve_actor_user($userid)
645 return $e->event unless $e->allowed( 'VIEW_HOLD', $patron->home_ou );
646 return __user_hold_count( $self, $e, $userid );
649 sub __user_hold_count {
650 my ( $self, $e, $userid ) = @_;
651 my $holds = $e->search_action_hold_request(
654 fulfillment_time => undef,
655 cancel_time => undef,
660 return scalar(@$holds);
664 __PACKAGE__->register_method(
665 method => "retrieve_holds_by_pickup_lib",
666 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
668 "Retrieves all the holds, with hold transits attached, for the specified pickup_ou id."
671 __PACKAGE__->register_method(
672 method => "retrieve_holds_by_pickup_lib",
673 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
674 notes => "Retrieves all the hold ids for the specified pickup_ou id. "
677 sub retrieve_holds_by_pickup_lib {
678 my ($self, $client, $login_session, $ou_id) = @_;
680 #FIXME -- put an appropriate permission check here
681 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
682 # $login_session, $user_id, 'VIEW_HOLD' );
683 #return $evt if $evt;
685 my $holds = $apputils->simplereq(
687 "open-ils.cstore.direct.action.hold_request.search.atomic",
689 pickup_lib => $ou_id ,
690 fulfillment_time => undef,
693 { order_by => { ahr => "request_time" } }
696 if ( ! $self->api_name =~ /id_list/ ) {
697 flesh_hold_transits($holds);
701 return [ map { $_->id } @$holds ];
705 __PACKAGE__->register_method(
706 method => "uncancel_hold",
707 api_name => "open-ils.circ.hold.uncancel"
711 my($self, $client, $auth, $hold_id) = @_;
712 my $e = new_editor(authtoken=>$auth, xact=>1);
713 return $e->die_event unless $e->checkauth;
715 my $hold = $e->retrieve_action_hold_request($hold_id)
716 or return $e->die_event;
717 return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
719 if ($hold->fulfillment_time) {
723 unless ($hold->cancel_time) {
728 # if configured to reset the request time, also reset the expire time
729 if($U->ou_ancestor_setting_value(
730 $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
732 $hold->request_time('now');
733 $hold->expire_time(calculate_expire_time($hold->request_lib));
736 $hold->clear_cancel_time;
737 $hold->clear_cancel_cause;
738 $hold->clear_cancel_note;
739 $hold->clear_shelf_time;
740 $hold->clear_current_copy;
741 $hold->clear_capture_time;
742 $hold->clear_prev_check_time;
743 $hold->clear_shelf_expire_time;
744 $hold->clear_current_shelf_lib;
746 $e->update_action_hold_request($hold) or return $e->die_event;
749 $U->simplereq('open-ils.hold-targeter',
750 'open-ils.hold-targeter.target', {hold => $hold_id});
756 __PACKAGE__->register_method(
757 method => "cancel_hold",
758 api_name => "open-ils.circ.hold.cancel",
760 desc => 'Cancels the specified hold. The login session is the requestor. If the requestor is different from the usr field ' .
761 'on the hold, the requestor must have CANCEL_HOLDS permissions. The hold may be either the hold object or the hold id',
763 {desc => 'Authentication token', type => 'string'},
764 {desc => 'Hold ID', type => 'number'},
765 {desc => 'Cause of Cancellation', type => 'string'},
766 {desc => 'Note', type => 'string'}
769 desc => '1 on success, event on error'
775 my($self, $client, $auth, $holdid, $cause, $note) = @_;
777 my $e = new_editor(authtoken=>$auth, xact=>1);
778 return $e->die_event unless $e->checkauth;
780 my $hold = $e->retrieve_action_hold_request($holdid)
781 or return $e->die_event;
783 if( $e->requestor->id ne $hold->usr ) {
784 return $e->die_event unless $e->allowed('CANCEL_HOLDS');
787 if ($hold->cancel_time) {
792 # If the hold is captured, reset the copy status
793 if( $hold->capture_time and $hold->current_copy ) {
795 my $copy = $e->retrieve_asset_copy($hold->current_copy)
796 or return $e->die_event;
798 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
799 $logger->info("canceling hold $holdid whose item is on the holds shelf");
800 # $logger->info("setting copy to status 'reshelving' on hold cancel");
801 # $copy->status(OILS_COPY_STATUS_RESHELVING);
802 # $copy->editor($e->requestor->id);
803 # $copy->edit_date('now');
804 # $e->update_asset_copy($copy) or return $e->event;
806 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
809 $logger->warn("! canceling hold [$hid] that is in transit");
810 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
813 my $trans = $e->retrieve_action_transit_copy($transid);
814 # Leave the transit alive, but set the copy status to
815 # reshelving so it will be properly reshelved when it gets back home
817 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
818 $e->update_action_transit_copy($trans) or return $e->die_event;
824 $hold->cancel_time('now');
825 $hold->cancel_cause($cause);
826 $hold->cancel_note($note);
827 $e->update_action_hold_request($hold)
828 or return $e->die_event;
832 # re-fetch the hold to pick up the real cancel_time (not "now") for A/T
834 $hold = $e->retrieve_action_hold_request($hold->id) or return $e->die_event;
837 if ($e->requestor->id == $hold->usr) {
838 $U->create_events_for_hook('hold_request.cancel.patron', $hold, $hold->pickup_lib);
840 $U->create_events_for_hook('hold_request.cancel.staff', $hold, $hold->pickup_lib);
846 my $update_hold_desc = 'The login session is the requestor. ' .
847 'If the requestor is different from the usr field on the hold, ' .
848 'the requestor must have UPDATE_HOLDS permissions. ' .
849 'If supplying a hash of hold data, "id" must be included. ' .
850 'The hash is ignored if a hold object is supplied, ' .
851 'so you should supply only one kind of hold data argument.' ;
853 __PACKAGE__->register_method(
854 method => "update_hold",
855 api_name => "open-ils.circ.hold.update",
857 desc => "Updates the specified hold. $update_hold_desc",
859 {desc => 'Authentication token', type => 'string'},
860 {desc => 'Hold Object', type => 'object'},
861 {desc => 'Hash of values to be applied', type => 'object'}
864 desc => 'Hold ID on success, event on error',
870 __PACKAGE__->register_method(
871 method => "batch_update_hold",
872 api_name => "open-ils.circ.hold.update.batch",
875 desc => "Updates the specified hold(s). $update_hold_desc",
877 {desc => 'Authentication token', type => 'string'},
878 {desc => 'Array of hold obejcts', type => 'array' },
879 {desc => 'Array of hashes of values to be applied', type => 'array' }
882 desc => 'Hold ID per success, event per error',
888 my($self, $client, $auth, $hold, $values) = @_;
889 my $e = new_editor(authtoken=>$auth, xact=>1);
890 return $e->die_event unless $e->checkauth;
891 my $resp = update_hold_impl($self, $e, $hold, $values);
892 if ($U->event_code($resp)) {
896 $e->commit; # FIXME: update_hold_impl already does $e->commit ??
900 sub batch_update_hold {
901 my($self, $client, $auth, $hold_list, $values_list) = @_;
902 my $e = new_editor(authtoken=>$auth);
903 return $e->die_event unless $e->checkauth;
905 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.
907 $values_list ||= []; # FIXME: either move this above $count declaration, or send an event if both lists undef. Probably the latter.
909 # FIXME: Failing over to [] guarantees warnings for "Use of unitialized value" in update_hold_impl call.
910 # FIXME: We should be sure we only call update_hold_impl with hold object OR hash, not both.
912 for my $idx (0..$count-1) {
914 my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
915 $e->xact_commit unless $U->event_code($resp);
916 $client->respond($resp);
920 return undef; # not in the register return type, assuming we should always have at least one list populated
923 sub update_hold_impl {
924 my($self, $e, $hold, $values) = @_;
926 my $need_retarget = 0;
929 $hold = $e->retrieve_action_hold_request($values->{id})
930 or return $e->die_event;
931 for my $k (keys %$values) {
932 # Outside of pickup_lib (covered by the first regex) I don't know when these would currently change.
933 # But hey, why not cover things that may happen later?
934 if ($k =~ '_(lib|ou)$' || $k eq 'target' || $k eq 'hold_type' || $k eq 'requestor' || $k eq 'selection_depth' || $k eq 'holdable_formats') {
935 if (defined $values->{$k} && defined $hold->$k() && $values->{$k} ne $hold->$k()) {
936 # Value changed? RETARGET!
938 } elsif (defined $hold->$k() != defined $values->{$k}) {
939 # Value being set or cleared? RETARGET!
943 if (defined $values->{$k}) {
944 $hold->$k($values->{$k});
946 my $f = "clear_$k"; $hold->$f();
951 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
952 or return $e->die_event;
954 # don't allow the user to be changed
955 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
957 if($hold->usr ne $e->requestor->id) {
958 # if the hold is for a different user, make sure the
959 # requestor has the appropriate permissions
960 my $usr = $e->retrieve_actor_user($hold->usr)
961 or return $e->die_event;
962 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
966 # --------------------------------------------------------------
967 # Changing the request time is like playing God
968 # --------------------------------------------------------------
969 if($hold->request_time ne $orig_hold->request_time) {
970 return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
971 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
975 # --------------------------------------------------------------
976 # Code for making sure staff have appropriate permissons for cut_in_line
977 # This, as is, doesn't prevent a user from cutting their own holds in line
979 # --------------------------------------------------------------
980 if($U->is_true($hold->cut_in_line) ne $U->is_true($orig_hold->cut_in_line)) {
981 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
985 # --------------------------------------------------------------
986 # Disallow hold suspencion if the hold is already captured.
987 # --------------------------------------------------------------
988 if ($U->is_true($hold->frozen) and not $U->is_true($orig_hold->frozen)) {
989 $hold_status = _hold_status($e, $hold);
990 if ($hold_status > 2 && $hold_status != 7) { # hold is captured
991 $logger->info("bypassing hold freeze on captured hold");
992 return OpenILS::Event->new('HOLD_SUSPEND_AFTER_CAPTURE');
997 # --------------------------------------------------------------
998 # if the hold is on the holds shelf or in transit and the pickup
999 # lib changes we need to create a new transit.
1000 # --------------------------------------------------------------
1001 if($orig_hold->pickup_lib ne $hold->pickup_lib) {
1003 $hold_status = _hold_status($e, $hold) unless $hold_status;
1005 if($hold_status == 3) { # in transit
1007 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
1008 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
1010 $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
1012 # update the transit to reflect the new pickup location
1013 my $transit = $e->search_action_hold_transit_copy(
1014 {hold=>$hold->id, cancel_time => undef, dest_recv_time => undef})->[0]
1015 or return $e->die_event;
1017 $transit->prev_dest($transit->dest); # mark the previous destination on the transit
1018 $transit->dest($hold->pickup_lib);
1019 $e->update_action_hold_transit_copy($transit) or return $e->die_event;
1021 } elsif($hold_status == 4 or $hold_status == 5 or $hold_status == 8) { # on holds shelf
1023 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
1024 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
1026 $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
1028 if ($hold->pickup_lib eq $orig_hold->current_shelf_lib) {
1029 # This can happen if the pickup lib is changed while the hold is
1030 # on the shelf, then changed back to the original pickup lib.
1031 # Restore the original shelf_expire_time to prevent abuse.
1032 set_hold_shelf_expire_time(undef, $hold, $e, $hold->shelf_time);
1035 # clear to prevent premature shelf expiration
1036 $hold->clear_shelf_expire_time;
1041 if($U->is_true($hold->frozen)) {
1042 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1043 $hold->clear_current_copy;
1044 $hold->clear_prev_check_time;
1045 # Clear expire_time to prevent frozen holds from expiring.
1046 $logger->info("clearing expire_time for frozen hold ".$hold->id);
1047 $hold->clear_expire_time;
1050 # If the hold_expire_time is in the past && is not equal to the
1051 # original expire_time, then reset the expire time to be in the
1053 if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1 && $U->datecmp($hold->expire_time, $orig_hold->expire_time) != 0) {
1054 $hold->expire_time(calculate_expire_time($hold->request_lib));
1057 # If the hold is reactivated, reset the expire_time.
1058 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1059 $logger->info("Reset expire_time on activated hold ".$hold->id);
1060 $hold->expire_time(calculate_expire_time($hold->request_lib));
1063 $e->update_action_hold_request($hold) or return $e->die_event;
1066 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1067 $logger->info("Running targeter on activated hold ".$hold->id);
1068 $U->simplereq('open-ils.hold-targeter',
1069 'open-ils.hold-targeter.target', {hold => $hold->id});
1072 # a change to mint-condition changes the set of potential copies, so retarget the hold;
1073 if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
1074 _reset_hold($self, $e->requestor, $hold)
1075 } elsif($need_retarget && !defined $hold->capture_time()) { # If needed, retarget the hold due to changes
1076 $U->simplereq('open-ils.hold-targeter',
1077 'open-ils.hold-targeter.target', {hold => $hold->id});
1083 # this does not update the hold in the DB. It only
1084 # sets the shelf_expire_time field on the hold object.
1085 # start_time is optional and defaults to 'now'
1086 sub set_hold_shelf_expire_time {
1087 my ($class, $hold, $editor, $start_time) = @_;
1089 my $shelf_expire = $U->ou_ancestor_setting_value(
1091 'circ.holds.default_shelf_expire_interval',
1095 return undef unless $shelf_expire;
1097 $start_time = ($start_time) ?
1098 DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) :
1099 DateTime->now(time_zone => 'local'); # without time_zone we get UTC ... yuck!
1101 my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
1102 my $expire_time = $start_time->add(seconds => $seconds);
1104 # if the shelf expire time overlaps with a pickup lib's
1105 # closed date, push it out to the first open date
1106 my $dateinfo = $U->storagereq(
1107 'open-ils.storage.actor.org_unit.closed_date.overlap',
1108 $hold->pickup_lib, $expire_time->strftime('%FT%T%z'));
1111 my $dt_parser = DateTime::Format::ISO8601->new;
1112 $expire_time = $dt_parser->parse_datetime(cleanse_ISO8601($dateinfo->{end}));
1114 # TODO: enable/disable time bump via setting?
1115 $expire_time->set(hour => '23', minute => '59', second => '59');
1117 $logger->info("circulator: shelf_expire_time overlaps".
1118 " with closed date, pushing expire time to $expire_time");
1121 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
1127 my($e, $orig_hold, $hold, $copy) = @_;
1128 my $src = $orig_hold->pickup_lib;
1129 my $dest = $hold->pickup_lib;
1131 $logger->info("putting hold into transit on pickup_lib update");
1133 my $transit = Fieldmapper::action::hold_transit_copy->new;
1134 $transit->hold($hold->id);
1135 $transit->source($src);
1136 $transit->dest($dest);
1137 $transit->target_copy($copy->id);
1138 $transit->source_send_time('now');
1139 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1141 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1142 $copy->editor($e->requestor->id);
1143 $copy->edit_date('now');
1145 $e->create_action_hold_transit_copy($transit) or return $e->die_event;
1146 $e->update_asset_copy($copy) or return $e->die_event;
1150 # if the hold is frozen, this method ensures that the hold is not "targeted",
1151 # that is, it clears the current_copy and prev_check_time to essentiallly
1152 # reset the hold. If it is being activated, it runs the targeter in the background
1153 sub update_hold_if_frozen {
1154 my($self, $e, $hold, $orig_hold) = @_;
1155 return if $hold->capture_time;
1157 if($U->is_true($hold->frozen)) {
1158 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1159 $hold->clear_current_copy;
1160 $hold->clear_prev_check_time;
1163 if($U->is_true($orig_hold->frozen)) {
1164 $logger->info("Running targeter on activated hold ".$hold->id);
1165 $U->simplereq('open-ils.hold-targeter',
1166 'open-ils.hold-targeter.target', {hold => $hold->id});
1171 __PACKAGE__->register_method(
1172 method => "hold_note_CUD",
1173 api_name => "open-ils.circ.hold_request.note.cud",
1175 desc => 'Create, update or delete a hold request note. If the operator (from Auth. token) '
1176 . 'is not the owner of the hold, the UPDATE_HOLD permission is required',
1178 { desc => 'Authentication token', type => 'string' },
1179 { desc => 'Hold note object', type => 'object' }
1182 desc => 'Returns the note ID, event on error'
1188 my($self, $conn, $auth, $note) = @_;
1190 my $e = new_editor(authtoken => $auth, xact => 1);
1191 return $e->die_event unless $e->checkauth;
1193 my $hold = $e->retrieve_action_hold_request($note->hold)
1194 or return $e->die_event;
1196 if($hold->usr ne $e->requestor->id) {
1197 my $usr = $e->retrieve_actor_user($hold->usr);
1198 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1199 $note->staff('t') if $note->isnew;
1203 $e->create_action_hold_request_note($note) or return $e->die_event;
1204 } elsif($note->ischanged) {
1205 $e->update_action_hold_request_note($note) or return $e->die_event;
1206 } elsif($note->isdeleted) {
1207 $e->delete_action_hold_request_note($note) or return $e->die_event;
1215 __PACKAGE__->register_method(
1216 method => "retrieve_hold_status",
1217 api_name => "open-ils.circ.hold.status.retrieve",
1219 desc => 'Calculates the current status of the hold. The requestor must have ' .
1220 'VIEW_HOLD permissions if the hold is for a user other than the requestor' ,
1222 { desc => 'Hold ID', type => 'number' }
1225 # type => 'number', # event sometimes
1226 desc => <<'END_OF_DESC'
1227 Returns event on error or:
1228 -1 on error (for now),
1229 1 for 'waiting for copy to become available',
1230 2 for 'waiting for copy capture',
1233 5 for 'hold-shelf-delay'
1236 8 for 'captured, on wrong hold shelf'
1243 sub retrieve_hold_status {
1244 my($self, $client, $auth, $hold_id) = @_;
1246 my $e = new_editor(authtoken => $auth);
1247 return $e->event unless $e->checkauth;
1248 my $hold = $e->retrieve_action_hold_request($hold_id)
1249 or return $e->event;
1251 if( $e->requestor->id != $hold->usr ) {
1252 return $e->event unless $e->allowed('VIEW_HOLD');
1255 return _hold_status($e, $hold);
1261 if ($hold->cancel_time) {
1264 if ($U->is_true($hold->frozen) && !$hold->capture_time) {
1267 if ($hold->current_shelf_lib and $hold->current_shelf_lib ne $hold->pickup_lib) {
1270 if ($hold->fulfillment_time) {
1273 return 1 unless $hold->current_copy;
1274 return 2 unless $hold->capture_time;
1276 my $copy = $hold->current_copy;
1277 unless( ref $copy ) {
1278 $copy = $e->retrieve_asset_copy($hold->current_copy)
1279 or return $e->event;
1282 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
1284 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
1286 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
1287 return 4 unless $hs_wait_interval;
1289 # if a hold_shelf_status_delay interval is defined and start_time plus
1290 # the interval is greater than now, consider the hold to be in the virtual
1291 # "on its way to the holds shelf" status. Return 5.
1293 my $transit = $e->search_action_hold_transit_copy({
1295 target_copy => $copy->id,
1296 cancel_time => undef,
1297 dest_recv_time => {'!=' => undef},
1299 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
1300 $start_time = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time));
1301 my $end_time = $start_time->add(seconds => OpenSRF::Utils::interval_to_seconds($hs_wait_interval));
1303 return 5 if $end_time > DateTime->now;
1312 __PACKAGE__->register_method(
1313 method => "retrieve_hold_queue_stats",
1314 api_name => "open-ils.circ.hold.queue_stats.retrieve",
1316 desc => 'Returns summary data about the state of a hold',
1318 { desc => 'Authentication token', type => 'string'},
1319 { desc => 'Hold ID', type => 'number'},
1322 desc => q/Summary object with keys:
1323 total_holds : total holds in queue
1324 queue_position : current queue position
1325 potential_copies : number of potential copies for this hold
1326 estimated_wait : estimated wait time in days
1327 status : hold status
1328 -1 => error or unexpected state,
1329 1 => 'waiting for copy to become available',
1330 2 => 'waiting for copy capture',
1333 5 => 'hold-shelf-delay'
1340 sub retrieve_hold_queue_stats {
1341 my($self, $conn, $auth, $hold_id) = @_;
1342 my $e = new_editor(authtoken => $auth);
1343 return $e->event unless $e->checkauth;
1344 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
1345 if($e->requestor->id != $hold->usr) {
1346 return $e->event unless $e->allowed('VIEW_HOLD');
1348 return retrieve_hold_queue_status_impl($e, $hold);
1351 sub retrieve_hold_queue_status_impl {
1355 # The holds queue is defined as the distinct set of holds that share at
1356 # least one potential copy with the context hold, plus any holds that
1357 # share the same hold type and target. The latter part exists to
1358 # accomodate holds that currently have no potential copies
1359 my $q_holds = $e->json_query({
1361 # fetch cut_in_line and request_time since they're in the order_by
1362 # and we're asking for distinct values
1363 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1367 select => { ahcm => ['hold'] },
1372 'field' => 'target_copy',
1373 'fkey' => 'target_copy'
1377 where => { '+ahcm2' => { hold => $hold->id } },
1384 "field" => "cut_in_line",
1385 "transform" => "coalesce",
1387 "direction" => "desc"
1389 { "class" => "ahr", "field" => "request_time" }
1394 if (!@$q_holds) { # none? maybe we don't have a map ...
1395 $q_holds = $e->json_query({
1396 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1401 "field" => "cut_in_line",
1402 "transform" => "coalesce",
1404 "direction" => "desc"
1406 { "class" => "ahr", "field" => "request_time" }
1409 hold_type => $hold->hold_type,
1410 target => $hold->target,
1411 capture_time => undef,
1412 cancel_time => undef,
1414 {expire_time => undef },
1415 {expire_time => {'>' => 'now'}}
1423 for my $h (@$q_holds) {
1424 last if $h->{id} == $hold->id;
1428 my $hold_data = $e->json_query({
1430 acp => [ {column => 'id', transform => 'count', aggregate => 1, alias => 'count'} ],
1431 ccm => [ {column =>'avg_wait_time'} ]
1437 ccm => {type => 'left'}
1442 where => {'+ahcm' => {hold => $hold->id} }
1445 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1447 my $default_wait = $U->ou_ancestor_setting_value(
1448 $user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL, $e);
1449 my $min_wait = $U->ou_ancestor_setting_value(
1450 $user_org, 'circ.holds.min_estimated_wait_interval', $e);
1451 $min_wait = OpenSRF::Utils::interval_to_seconds($min_wait || '0 seconds');
1452 $default_wait ||= '0 seconds';
1454 # Estimated wait time is the average wait time across the set
1455 # of potential copies, divided by the number of potential copies
1456 # times the queue position.
1458 my $combined_secs = 0;
1459 my $num_potentials = 0;
1461 for my $wait_data (@$hold_data) {
1462 my $count += $wait_data->{count};
1463 $combined_secs += $count *
1464 OpenSRF::Utils::interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
1465 $num_potentials += $count;
1468 my $estimated_wait = -1;
1470 if($num_potentials) {
1471 my $avg_wait = $combined_secs / $num_potentials;
1472 $estimated_wait = $qpos * ($avg_wait / $num_potentials);
1473 $estimated_wait = $min_wait if $estimated_wait < $min_wait and $estimated_wait != -1;
1477 total_holds => scalar(@$q_holds),
1478 queue_position => $qpos,
1479 potential_copies => $num_potentials,
1480 status => _hold_status( $e, $hold ),
1481 estimated_wait => int($estimated_wait)
1486 sub fetch_open_hold_by_current_copy {
1489 my $hold = $apputils->simplereq(
1491 'open-ils.cstore.direct.action.hold_request.search.atomic',
1492 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1493 return $hold->[0] if ref($hold);
1497 sub fetch_related_holds {
1500 return $apputils->simplereq(
1502 'open-ils.cstore.direct.action.hold_request.search.atomic',
1503 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1507 __PACKAGE__->register_method(
1508 method => "hold_pull_list",
1509 api_name => "open-ils.circ.hold_pull_list.retrieve",
1511 desc => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1512 'The location is determined by the login session.',
1514 { desc => 'Limit (optional)', type => 'number'},
1515 { desc => 'Offset (optional)', type => 'number'},
1518 desc => 'reference to a list of holds, or event on failure',
1523 __PACKAGE__->register_method(
1524 method => "hold_pull_list",
1525 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
1527 desc => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
1528 'The location is determined by the login session.',
1530 { desc => 'Limit (optional)', type => 'number'},
1531 { desc => 'Offset (optional)', type => 'number'},
1534 desc => 'reference to a list of holds, or event on failure',
1539 __PACKAGE__->register_method(
1540 method => "hold_pull_list",
1541 api_name => "open-ils.circ.hold_pull_list.retrieve.count",
1543 desc => 'Returns a count of holds that need to be "pulled" by a given location. ' .
1544 'The location is determined by the login session.',
1546 { desc => 'Limit (optional)', type => 'number'},
1547 { desc => 'Offset (optional)', type => 'number'},
1550 desc => 'Holds count (integer), or event on failure',
1556 __PACKAGE__->register_method(
1557 method => "hold_pull_list",
1559 # TODO: tag with api_level 2 once fully supported
1560 api_name => "open-ils.circ.hold_pull_list.fleshed.stream",
1562 desc => q/Returns a stream of fleshed holds that need to be
1563 "pulled" by a given location. The location is
1564 determined by the login session.
1565 This API calls always run in authoritative mode./,
1567 { desc => 'Limit (optional)', type => 'number'},
1568 { desc => 'Offset (optional)', type => 'number'},
1571 desc => 'Stream of holds holds, or event on failure',
1576 sub hold_pull_list {
1577 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1578 my( $reqr, $evt ) = $U->checkses($authtoken);
1579 return $evt if $evt;
1581 my $org = $reqr->ws_ou || $reqr->home_ou;
1582 # the perm locaiton shouldn't really matter here since holds
1583 # will exist all over and VIEW_HOLDS should be universal
1584 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1585 return $evt if $evt;
1587 if($self->api_name =~ /count/) {
1589 my $count = $U->storagereq(
1590 'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1591 $org, $limit, $offset );
1593 $logger->info("Grabbing pull list for org unit $org with $count items");
1596 } elsif( $self->api_name =~ /id_list/ ) {
1598 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1599 $org, $limit, $offset );
1601 } elsif ($self->api_name =~ /fleshed/) {
1603 my $ids = $U->storagereq(
1604 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1605 $org, $limit, $offset );
1607 my $e = new_editor(xact => 1, requestor => $reqr);
1608 $conn->respond(uber_hold_impl($e, $_, {flesh_acpl => 1})) for @$ids;
1610 $conn->respond_complete;
1615 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1616 $org, $limit, $offset );
1620 __PACKAGE__->register_method(
1621 method => "print_hold_pull_list",
1622 api_name => "open-ils.circ.hold_pull_list.print",
1624 desc => 'Returns an HTML-formatted holds pull list',
1626 { desc => 'Authtoken', type => 'string'},
1627 { desc => 'Org unit ID. Optional, defaults to workstation org unit', type => 'number'},
1630 desc => 'HTML string',
1636 sub print_hold_pull_list {
1637 my($self, $client, $auth, $org_id) = @_;
1639 my $e = new_editor(authtoken=>$auth);
1640 return $e->event unless $e->checkauth;
1642 $org_id = (defined $org_id) ? $org_id : $e->requestor->ws_ou;
1643 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1645 my $hold_ids = $U->storagereq(
1646 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1649 return undef unless @$hold_ids;
1651 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1653 # Holds will /NOT/ be in order after this ...
1654 my $holds = $e->search_action_hold_request({id => $hold_ids}, {substream => 1});
1655 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1657 # ... so we must resort.
1658 my $hold_map = +{map { $_->id => $_ } @$holds};
1659 my $sorted_holds = [];
1660 push @$sorted_holds, $hold_map->{$_} foreach @$hold_ids;
1662 return $U->fire_object_event(
1663 undef, "ahr.format.pull_list", $sorted_holds,
1664 $org_id, undef, undef, $client
1669 __PACKAGE__->register_method(
1670 method => "print_hold_pull_list_stream",
1672 api_name => "open-ils.circ.hold_pull_list.print.stream",
1674 desc => 'Returns a stream of fleshed holds',
1676 { desc => 'Authtoken', type => 'string'},
1677 { 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)',
1682 desc => 'A stream of fleshed holds',
1688 sub print_hold_pull_list_stream {
1689 my($self, $client, $auth, $params) = @_;
1691 my $e = new_editor(authtoken=>$auth);
1692 return $e->die_event unless $e->checkauth;
1694 delete($$params{org_id}) unless (int($$params{org_id}));
1695 delete($$params{limit}) unless (int($$params{limit}));
1696 delete($$params{offset}) unless (int($$params{offset}));
1697 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
1698 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
1699 $$params{chunk_size} ||= 10;
1700 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
1702 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
1703 return $e->die_event unless $e->allowed('VIEW_HOLD', $$params{org_id });
1706 if ($$params{sort} && @{ $$params{sort} }) {
1707 for my $s (@{ $$params{sort} }) {
1708 if ($s eq 'acplo.position') {
1710 "class" => "acplo", "field" => "position",
1711 "transform" => "coalesce", "params" => [999]
1713 } elsif ($s eq 'prefix') {
1714 push @$sort, {"class" => "acnp", "field" => "label_sortkey"};
1715 } elsif ($s eq 'call_number') {
1716 push @$sort, {"class" => "acn", "field" => "label_sortkey"};
1717 } elsif ($s eq 'suffix') {
1718 push @$sort, {"class" => "acns", "field" => "label_sortkey"};
1719 } elsif ($s eq 'request_time') {
1720 push @$sort, {"class" => "ahr", "field" => "request_time"};
1724 push @$sort, {"class" => "ahr", "field" => "request_time"};
1727 my $holds_ids = $e->json_query(
1729 "select" => {"ahr" => ["id"]},
1734 "fkey" => "current_copy",
1736 "circ_lib" => $$params{org_id}, "status" => [0,7]
1741 "fkey" => "call_number",
1755 "fkey" => "circ_lib",
1758 "location" => {"=" => {"+acp" => "location"}}
1767 "capture_time" => undef,
1768 "cancel_time" => undef,
1770 {"expire_time" => undef },
1771 {"expire_time" => {">" => "now"}}
1775 (@$sort ? (order_by => $sort) : ()),
1776 ($$params{limit} ? (limit => $$params{limit}) : ()),
1777 ($$params{offset} ? (offset => $$params{offset}) : ())
1778 }, {"substream" => 1}
1779 ) or return $e->die_event;
1781 $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
1784 for my $hid (@$holds_ids) {
1785 push @chunk, $e->retrieve_action_hold_request([
1789 "ahr" => ["usr", "current_copy"],
1791 "acp" => ["location", "call_number", "parts"],
1792 "acn" => ["record","prefix","suffix"]
1797 if (@chunk >= $$params{chunk_size}) {
1798 $client->respond( \@chunk );
1802 $client->respond_complete( \@chunk ) if (@chunk);
1809 __PACKAGE__->register_method(
1810 method => 'fetch_hold_notify',
1811 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
1814 Returns a list of hold notification objects based on hold id.
1815 @param authtoken The loggin session key
1816 @param holdid The id of the hold whose notifications we want to retrieve
1817 @return An array of hold notification objects, event on error.
1821 sub fetch_hold_notify {
1822 my( $self, $conn, $authtoken, $holdid ) = @_;
1823 my( $requestor, $evt ) = $U->checkses($authtoken);
1824 return $evt if $evt;
1825 my ($hold, $patron);
1826 ($hold, $evt) = $U->fetch_hold($holdid);
1827 return $evt if $evt;
1828 ($patron, $evt) = $U->fetch_user($hold->usr);
1829 return $evt if $evt;
1831 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
1832 return $evt if $evt;
1834 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
1835 return $U->cstorereq(
1836 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
1840 __PACKAGE__->register_method(
1841 method => 'create_hold_notify',
1842 api_name => 'open-ils.circ.hold_notification.create',
1844 Creates a new hold notification object
1845 @param authtoken The login session key
1846 @param notification The hold notification object to create
1847 @return ID of the new object on success, Event on error
1851 sub create_hold_notify {
1852 my( $self, $conn, $auth, $note ) = @_;
1853 my $e = new_editor(authtoken=>$auth, xact=>1);
1854 return $e->die_event unless $e->checkauth;
1856 my $hold = $e->retrieve_action_hold_request($note->hold)
1857 or return $e->die_event;
1858 my $patron = $e->retrieve_actor_user($hold->usr)
1859 or return $e->die_event;
1861 return $e->die_event unless
1862 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
1864 $note->notify_staff($e->requestor->id);
1865 $e->create_action_hold_notification($note) or return $e->die_event;
1870 __PACKAGE__->register_method(
1871 method => 'create_hold_note',
1872 api_name => 'open-ils.circ.hold_note.create',
1874 Creates a new hold request note object
1875 @param authtoken The login session key
1876 @param note The hold note object to create
1877 @return ID of the new object on success, Event on error
1881 sub create_hold_note {
1882 my( $self, $conn, $auth, $note ) = @_;
1883 my $e = new_editor(authtoken=>$auth, xact=>1);
1884 return $e->die_event unless $e->checkauth;
1886 my $hold = $e->retrieve_action_hold_request($note->hold)
1887 or return $e->die_event;
1888 my $patron = $e->retrieve_actor_user($hold->usr)
1889 or return $e->die_event;
1891 return $e->die_event unless
1892 $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
1894 $e->create_action_hold_request_note($note) or return $e->die_event;
1899 __PACKAGE__->register_method(
1900 method => 'reset_hold',
1901 api_name => 'open-ils.circ.hold.reset',
1903 Un-captures and un-targets a hold, essentially returning
1904 it to the state it was in directly after it was placed,
1905 then attempts to re-target the hold
1906 @param authtoken The login session key
1907 @param holdid The id of the hold
1913 my( $self, $conn, $auth, $holdid ) = @_;
1915 my ($hold, $evt) = $U->fetch_hold($holdid);
1916 return $evt if $evt;
1917 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1918 return $evt if $evt;
1919 $evt = _reset_hold($self, $reqr, $hold);
1920 return $evt if $evt;
1925 __PACKAGE__->register_method(
1926 method => 'reset_hold_batch',
1927 api_name => 'open-ils.circ.hold.reset.batch'
1930 sub reset_hold_batch {
1931 my($self, $conn, $auth, $hold_ids) = @_;
1933 my $e = new_editor(authtoken => $auth);
1934 return $e->event unless $e->checkauth;
1936 for my $hold_id ($hold_ids) {
1938 my $hold = $e->retrieve_action_hold_request(
1939 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
1940 or return $e->event;
1942 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1943 _reset_hold($self, $e->requestor, $hold);
1951 my ($self, $reqr, $hold) = @_;
1953 my $e = new_editor(xact =>1, requestor => $reqr);
1955 $logger->info("reseting hold ".$hold->id);
1957 my $hid = $hold->id;
1959 if( $hold->capture_time and $hold->current_copy ) {
1961 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1962 or return $e->die_event;
1964 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1965 $logger->info("setting copy to status 'reshelving' on hold retarget");
1966 $copy->status(OILS_COPY_STATUS_RESHELVING);
1967 $copy->editor($e->requestor->id);
1968 $copy->edit_date('now');
1969 $e->update_asset_copy($copy) or return $e->die_event;
1971 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1973 # We don't want the copy to remain "in transit"
1974 $copy->status(OILS_COPY_STATUS_RESHELVING);
1975 $logger->warn("! reseting hold [$hid] that is in transit");
1976 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id,cancel_time=>undef},{idlist=>1})->[0];
1979 my $trans = $e->retrieve_action_transit_copy($transid);
1981 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1982 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
1983 $logger->info("Transit abort completed with result $evt");
1984 unless ("$evt" eq 1) {
1993 $hold->clear_capture_time;
1994 $hold->clear_current_copy;
1995 $hold->clear_shelf_time;
1996 $hold->clear_shelf_expire_time;
1997 $hold->clear_current_shelf_lib;
1999 $e->update_action_hold_request($hold) or return $e->die_event;
2002 $U->simplereq('open-ils.hold-targeter',
2003 'open-ils.hold-targeter.target', {hold => $hold->id});
2009 __PACKAGE__->register_method(
2010 method => 'fetch_open_title_holds',
2011 api_name => 'open-ils.circ.open_holds.retrieve',
2013 Returns a list ids of un-fulfilled holds for a given title id
2014 @param authtoken The login session key
2015 @param id the id of the item whose holds we want to retrieve
2016 @param type The hold type - M, T, I, V, C, F, R
2020 sub fetch_open_title_holds {
2021 my( $self, $conn, $auth, $id, $type, $org ) = @_;
2022 my $e = new_editor( authtoken => $auth );
2023 return $e->event unless $e->checkauth;
2026 $org ||= $e->requestor->ws_ou;
2028 # return $e->search_action_hold_request(
2029 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
2031 # XXX make me return IDs in the future ^--
2032 my $holds = $e->search_action_hold_request(
2035 cancel_time => undef,
2037 fulfillment_time => undef
2041 flesh_hold_transits($holds);
2046 sub flesh_hold_transits {
2048 for my $hold ( @$holds ) {
2050 $apputils->simplereq(
2052 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
2053 { hold => $hold->id, cancel_time => undef },
2054 { order_by => { ahtc => 'id desc' }, limit => 1 }
2060 sub flesh_hold_notices {
2061 my( $holds, $e ) = @_;
2062 $e ||= new_editor();
2064 for my $hold (@$holds) {
2065 my $notices = $e->search_action_hold_notification(
2067 { hold => $hold->id },
2068 { order_by => { anh => 'notify_time desc' } },
2073 $hold->notify_count(scalar(@$notices));
2075 my $n = $e->retrieve_action_hold_notification($$notices[0])
2076 or return $e->event;
2077 $hold->notify_time($n->notify_time);
2083 __PACKAGE__->register_method(
2084 method => 'fetch_captured_holds',
2085 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
2089 Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
2090 @param authtoken The login session key
2091 @param org The org id of the location in question
2092 @param match_copy A specific copy to limit to
2096 __PACKAGE__->register_method(
2097 method => 'fetch_captured_holds',
2098 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
2102 Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
2103 @param authtoken The login session key
2104 @param org The org id of the location in question
2105 @param match_copy A specific copy to limit to
2109 __PACKAGE__->register_method(
2110 method => 'fetch_captured_holds',
2111 api_name => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
2115 Returns list ids of shelf-expired un-fulfilled holds for a given title id
2116 @param authtoken The login session key
2117 @param org The org id of the location in question
2118 @param match_copy A specific copy to limit to
2122 __PACKAGE__->register_method(
2123 method => 'fetch_captured_holds',
2125 'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
2129 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2130 for a given shelf lib
2134 __PACKAGE__->register_method(
2135 method => 'fetch_captured_holds',
2137 'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
2141 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2142 for a given shelf lib
2147 sub fetch_captured_holds {
2148 my( $self, $conn, $auth, $org, $match_copy ) = @_;
2150 my $e = new_editor(authtoken => $auth);
2151 return $e->die_event unless $e->checkauth;
2152 return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
2154 $org ||= $e->requestor->ws_ou;
2156 my $current_copy = { '!=' => undef };
2157 $current_copy = { '=' => $match_copy } if $match_copy;
2160 select => { alhr => ['id'] },
2165 fkey => 'current_copy'
2170 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
2172 capture_time => { "!=" => undef },
2173 current_copy => $current_copy,
2174 fulfillment_time => undef,
2175 current_shelf_lib => $org
2179 if($self->api_name =~ /expired/) {
2180 $query->{'where'}->{'+alhr'}->{'-or'} = {
2181 shelf_expire_time => { '<' => 'today'},
2182 cancel_time => { '!=' => undef },
2185 my $hold_ids = $e->json_query( $query );
2187 if ($self->api_name =~ /wrong_shelf/) {
2188 # fetch holds whose current_shelf_lib is $org, but whose pickup
2189 # lib is some other org unit. Ignore already-retrieved holds.
2191 pickup_lib_changed_on_shelf_holds(
2192 $e, $org, [map {$_->{id}} @$hold_ids]);
2193 # match the layout of other items in $hold_ids
2194 push (@$hold_ids, {id => $_}) for @$wrong_shelf;
2198 for my $hold_id (@$hold_ids) {
2199 if($self->api_name =~ /id_list/) {
2200 $conn->respond($hold_id->{id});
2204 $e->retrieve_action_hold_request([
2208 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
2209 order_by => {anh => 'notify_time desc'}
2219 __PACKAGE__->register_method(
2220 method => "print_expired_holds_stream",
2221 api_name => "open-ils.circ.captured_holds.expired.print.stream",
2225 sub print_expired_holds_stream {
2226 my ($self, $client, $auth, $params) = @_;
2228 # No need to check specific permissions: we're going to call another method
2229 # that will do that.
2230 my $e = new_editor("authtoken" => $auth);
2231 return $e->die_event unless $e->checkauth;
2233 delete($$params{org_id}) unless (int($$params{org_id}));
2234 delete($$params{limit}) unless (int($$params{limit}));
2235 delete($$params{offset}) unless (int($$params{offset}));
2236 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2237 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2238 $$params{chunk_size} ||= 10;
2239 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2241 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2243 my @hold_ids = $self->method_lookup(
2244 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
2245 )->run($auth, $params->{"org_id"});
2250 } elsif (defined $U->event_code($hold_ids[0])) {
2252 return $hold_ids[0];
2255 $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
2258 my @hid_chunk = splice @hold_ids, 0, $params->{"chunk_size"};
2260 my $result_chunk = $e->json_query({
2262 "acp" => ["barcode"],
2264 first_given_name second_given_name family_name alias
2273 "field" => "id", "fkey" => "current_copy",
2276 "field" => "id", "fkey" => "call_number",
2279 "field" => "id", "fkey" => "record"
2283 "acpl" => {"field" => "id", "fkey" => "location"}
2286 "au" => {"field" => "id", "fkey" => "usr"}
2289 "where" => {"+ahr" => {"id" => \@hid_chunk}}
2290 }) or return $e->die_event;
2291 $client->respond($result_chunk);
2298 __PACKAGE__->register_method(
2299 method => "check_title_hold_batch",
2300 api_name => "open-ils.circ.title_hold.is_possible.batch",
2303 desc => '@see open-ils.circ.title_hold.is_possible.batch',
2305 { desc => 'Authentication token', type => 'string'},
2306 { desc => 'Array of Hash of named parameters', type => 'array'},
2309 desc => 'Array of response objects',
2315 sub check_title_hold_batch {
2316 my($self, $client, $authtoken, $param_list, $oargs) = @_;
2317 foreach (@$param_list) {
2318 my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
2319 $client->respond($res);
2325 __PACKAGE__->register_method(
2326 method => "check_title_hold",
2327 api_name => "open-ils.circ.title_hold.is_possible",
2329 desc => 'Determines if a hold were to be placed by a given user, ' .
2330 'whether or not said hold would have any potential copies to fulfill it.' .
2331 'The named paramaters of the second argument include: ' .
2332 'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
2333 'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
2335 { desc => 'Authentication token', type => 'string'},
2336 { desc => 'Hash of named parameters', type => 'object'},
2339 desc => 'List of new message IDs (empty if none)',
2345 =head3 check_title_hold (token, hash)
2347 The named fields in the hash are:
2349 patronid - ID of the hold recipient (required)
2350 depth - hold range depth (default 0)
2351 pickup_lib - destination for hold, fallback value for selection_ou
2352 selection_ou - ID of org_unit establishing hard and soft hold boundary settings
2353 issuanceid - ID of the issuance to be held, required for Issuance level hold
2354 partid - ID of the monograph part to be held, required for monograph part level hold
2355 titleid - ID (BRN) of the title to be held, required for Title level hold
2356 volume_id - required for Volume level hold
2357 copy_id - required for Copy level hold
2358 mrid - required for Meta-record level hold
2359 hold_type - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record (default "T")
2361 All key/value pairs are passed on to do_possibility_checks.
2365 # FIXME: better params checking. what other params are required, if any?
2366 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
2367 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
2368 # used in conditionals, where it may be undefined, causing a warning.
2369 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
2371 sub check_title_hold {
2372 my( $self, $client, $authtoken, $params ) = @_;
2373 my $e = new_editor(authtoken=>$authtoken);
2374 return $e->event unless $e->checkauth;
2376 my %params = %$params;
2377 my $depth = $params{depth} || 0;
2378 my $selection_ou = $params{selection_ou} || $params{pickup_lib};
2379 my $oargs = $params{oargs} || {};
2381 if($oargs->{events}) {
2382 @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
2386 my $patron = $e->retrieve_actor_user($params{patronid})
2387 or return $e->event;
2389 if( $e->requestor->id ne $patron->id ) {
2390 return $e->event unless
2391 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
2394 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
2396 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
2397 or return $e->event;
2399 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
2400 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
2403 my $return_depth = $hard_boundary; # default depth to return on success
2404 if(defined $soft_boundary and $depth < $soft_boundary) {
2405 # work up the tree and as soon as we find a potential copy, use that depth
2406 # also, make sure we don't go past the hard boundary if it exists
2408 # our min boundary is the greater of user-specified boundary or hard boundary
2409 my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
2410 $hard_boundary : $depth;
2412 my $depth = $soft_boundary;
2413 while($depth >= $min_depth) {
2414 $logger->info("performing hold possibility check with soft boundary $depth");
2415 @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
2417 $return_depth = $depth;
2422 } elsif(defined $hard_boundary and $depth < $hard_boundary) {
2423 # there is no soft boundary, enforce the hard boundary if it exists
2424 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
2425 @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
2427 # no boundaries defined, fall back to user specifed boundary or no boundary
2428 $logger->info("performing hold possibility check with no boundary");
2429 @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
2432 my $place_unfillable = 0;
2433 $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
2438 "depth" => $return_depth,
2439 "local_avail" => $status[1]
2441 } elsif ($status[2]) {
2442 my $n = scalar @{$status[2]};
2443 return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2445 return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2451 sub do_possibility_checks {
2452 my($e, $patron, $request_lib, $depth, %params) = @_;
2454 my $issuanceid = $params{issuanceid} || "";
2455 my $partid = $params{partid} || "";
2456 my $titleid = $params{titleid} || "";
2457 my $volid = $params{volume_id};
2458 my $copyid = $params{copy_id};
2459 my $mrid = $params{mrid} || "";
2460 my $pickup_lib = $params{pickup_lib};
2461 my $hold_type = $params{hold_type} || 'T';
2462 my $selection_ou = $params{selection_ou} || $pickup_lib;
2463 my $holdable_formats = $params{holdable_formats};
2464 my $oargs = $params{oargs} || {};
2471 if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
2473 return $e->event unless $copy = $e->retrieve_asset_copy($copyid);
2474 return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
2475 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2477 return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
2478 return verify_copy_for_hold(
2479 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
2482 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2484 return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
2485 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2487 return _check_volume_hold_is_possible(
2488 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2491 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
2493 return _check_title_hold_is_possible(
2494 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
2497 } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
2499 return _check_issuance_hold_is_possible(
2500 $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2503 } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
2505 return _check_monopart_hold_is_possible(
2506 $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2509 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2511 # pasing undef as the depth to filtered_records causes the depth
2512 # of the selection_ou to be used, which is not what we want here.
2515 my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
2517 for my $rec (@$recs) {
2518 @status = _check_title_hold_is_possible(
2519 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
2525 # else { Unrecognized hold_type ! } # FIXME: return error? or 0?
2528 sub MR_filter_records {
2535 my $opac_visible = shift;
2537 my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
2538 return $U->storagereq(
2539 'open-ils.storage.metarecord.filtered_records.atomic',
2540 $m, $f, $org_at_depth, $opac_visible
2543 __PACKAGE__->register_method(
2544 method => 'MR_filter_records',
2545 api_name => 'open-ils.circ.holds.metarecord.filtered_records',
2550 sub create_ranged_org_filter {
2551 my($e, $selection_ou, $depth) = @_;
2553 # find the orgs from which this hold may be fulfilled,
2554 # based on the selection_ou and depth
2556 my $top_org = $e->search_actor_org_unit([
2557 {parent_ou => undef},
2558 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
2561 return () if $depth == $top_org->ou_type->depth;
2563 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
2564 %org_filter = (circ_lib => []);
2565 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
2567 $logger->info("hold org filter at depth $depth and selection_ou ".
2568 "$selection_ou created list of @{$org_filter{circ_lib}}");
2574 sub _check_title_hold_is_possible {
2575 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
2576 # $holdable_formats is now unused. We pre-filter the MR's records.
2578 my $e = new_editor();
2579 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2581 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2582 my $copies = $e->json_query(
2584 select => { acp => ['id', 'circ_lib'] },
2589 fkey => 'call_number',
2590 filter => { record => $titleid }
2594 filter => { holdable => 't', deleted => 'f' },
2597 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' },
2598 acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2602 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2603 '+acpm' => { target_copy => undef } # ignore part-linked copies
2608 $logger->info("title possible found ".scalar(@$copies)." potential copies");
2612 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2613 "payload" => {"fail_part" => "no_ultimate_items"}
2618 # -----------------------------------------------------------------------
2619 # sort the copies into buckets based on their circ_lib proximity to
2620 # the patron's home_ou.
2621 # -----------------------------------------------------------------------
2623 my $home_org = $patron->home_ou;
2624 my $req_org = $request_lib->id;
2626 $prox_cache{$home_org} =
2627 $e->search_actor_org_unit_proximity({from_org => $home_org})
2628 unless $prox_cache{$home_org};
2629 my $home_prox = $prox_cache{$home_org};
2630 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2633 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2634 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2636 my @keys = sort { $a <=> $b } keys %buckets;
2639 if( $home_org ne $req_org ) {
2640 # -----------------------------------------------------------------------
2641 # shove the copies close to the request_lib into the primary buckets
2642 # directly before the farthest away copies. That way, they are not
2643 # given priority, but they are checked before the farthest copies.
2644 # -----------------------------------------------------------------------
2645 $prox_cache{$req_org} =
2646 $e->search_actor_org_unit_proximity({from_org => $req_org})
2647 unless $prox_cache{$req_org};
2648 my $req_prox = $prox_cache{$req_org};
2651 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2652 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2654 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2655 my $new_key = $highest_key - 0.5; # right before the farthest prox
2656 my @keys2 = sort { $a <=> $b } keys %buckets2;
2657 for my $key (@keys2) {
2658 last if $key >= $highest_key;
2659 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2663 @keys = sort { $a <=> $b } keys %buckets;
2668 my $age_protect_only = 0;
2669 OUTER: for my $key (@keys) {
2670 my @cps = @{$buckets{$key}};
2672 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2674 for my $copyid (@cps) {
2676 next if $seen{$copyid};
2677 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2678 my $copy = $e->retrieve_asset_copy($copyid);
2679 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2681 unless($title) { # grab the title if we don't already have it
2682 my $vol = $e->retrieve_asset_call_number(
2683 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2684 $title = $vol->record;
2687 @status = verify_copy_for_hold(
2688 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2690 $age_protect_only ||= $status[3];
2691 last OUTER if $status[0];
2695 $status[3] = $age_protect_only;
2699 sub _check_issuance_hold_is_possible {
2700 my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2702 my $e = new_editor();
2703 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2705 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2706 my $copies = $e->json_query(
2708 select => { acp => ['id', 'circ_lib'] },
2714 filter => { issuance => $issuanceid }
2718 filter => { holdable => 't', deleted => 'f' },
2721 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
2725 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
2731 $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
2735 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
2736 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2741 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2742 "payload" => {"fail_part" => "no_ultimate_items"}
2750 # -----------------------------------------------------------------------
2751 # sort the copies into buckets based on their circ_lib proximity to
2752 # the patron's home_ou.
2753 # -----------------------------------------------------------------------
2755 my $home_org = $patron->home_ou;
2756 my $req_org = $request_lib->id;
2758 $prox_cache{$home_org} =
2759 $e->search_actor_org_unit_proximity({from_org => $home_org})
2760 unless $prox_cache{$home_org};
2761 my $home_prox = $prox_cache{$home_org};
2762 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2765 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2766 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2768 my @keys = sort { $a <=> $b } keys %buckets;
2771 if( $home_org ne $req_org ) {
2772 # -----------------------------------------------------------------------
2773 # shove the copies close to the request_lib into the primary buckets
2774 # directly before the farthest away copies. That way, they are not
2775 # given priority, but they are checked before the farthest copies.
2776 # -----------------------------------------------------------------------
2777 $prox_cache{$req_org} =
2778 $e->search_actor_org_unit_proximity({from_org => $req_org})
2779 unless $prox_cache{$req_org};
2780 my $req_prox = $prox_cache{$req_org};
2783 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2784 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2786 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2787 my $new_key = $highest_key - 0.5; # right before the farthest prox
2788 my @keys2 = sort { $a <=> $b } keys %buckets2;
2789 for my $key (@keys2) {
2790 last if $key >= $highest_key;
2791 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2795 @keys = sort { $a <=> $b } keys %buckets;
2800 my $age_protect_only = 0;
2801 OUTER: for my $key (@keys) {
2802 my @cps = @{$buckets{$key}};
2804 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2806 for my $copyid (@cps) {
2808 next if $seen{$copyid};
2809 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2810 my $copy = $e->retrieve_asset_copy($copyid);
2811 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2813 unless($title) { # grab the title if we don't already have it
2814 my $vol = $e->retrieve_asset_call_number(
2815 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2816 $title = $vol->record;
2819 @status = verify_copy_for_hold(
2820 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2822 $age_protect_only ||= $status[3];
2823 last OUTER if $status[0];
2828 if (!defined($empty_ok)) {
2829 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
2830 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2833 return (1,0) if ($empty_ok);
2835 $status[3] = $age_protect_only;
2839 sub _check_monopart_hold_is_possible {
2840 my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2842 my $e = new_editor();
2843 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2845 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2846 my $copies = $e->json_query(
2848 select => { acp => ['id', 'circ_lib'] },
2852 field => 'target_copy',
2854 filter => { part => $partid }
2858 filter => { holdable => 't', deleted => 'f' },
2861 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
2865 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
2871 $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
2875 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
2876 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2881 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2882 "payload" => {"fail_part" => "no_ultimate_items"}
2890 # -----------------------------------------------------------------------
2891 # sort the copies into buckets based on their circ_lib proximity to
2892 # the patron's home_ou.
2893 # -----------------------------------------------------------------------
2895 my $home_org = $patron->home_ou;
2896 my $req_org = $request_lib->id;
2898 $prox_cache{$home_org} =
2899 $e->search_actor_org_unit_proximity({from_org => $home_org})
2900 unless $prox_cache{$home_org};
2901 my $home_prox = $prox_cache{$home_org};
2902 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2905 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2906 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2908 my @keys = sort { $a <=> $b } keys %buckets;
2911 if( $home_org ne $req_org ) {
2912 # -----------------------------------------------------------------------
2913 # shove the copies close to the request_lib into the primary buckets
2914 # directly before the farthest away copies. That way, they are not
2915 # given priority, but they are checked before the farthest copies.
2916 # -----------------------------------------------------------------------
2917 $prox_cache{$req_org} =
2918 $e->search_actor_org_unit_proximity({from_org => $req_org})
2919 unless $prox_cache{$req_org};
2920 my $req_prox = $prox_cache{$req_org};
2923 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2924 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2926 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2927 my $new_key = $highest_key - 0.5; # right before the farthest prox
2928 my @keys2 = sort { $a <=> $b } keys %buckets2;
2929 for my $key (@keys2) {
2930 last if $key >= $highest_key;
2931 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2935 @keys = sort { $a <=> $b } keys %buckets;
2940 my $age_protect_only = 0;
2941 OUTER: for my $key (@keys) {
2942 my @cps = @{$buckets{$key}};
2944 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2946 for my $copyid (@cps) {
2948 next if $seen{$copyid};
2949 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2950 my $copy = $e->retrieve_asset_copy($copyid);
2951 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2953 unless($title) { # grab the title if we don't already have it
2954 my $vol = $e->retrieve_asset_call_number(
2955 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2956 $title = $vol->record;
2959 @status = verify_copy_for_hold(
2960 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2962 $age_protect_only ||= $status[3];
2963 last OUTER if $status[0];
2968 if (!defined($empty_ok)) {
2969 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
2970 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2973 return (1,0) if ($empty_ok);
2975 $status[3] = $age_protect_only;
2980 sub _check_volume_hold_is_possible {
2981 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2982 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
2983 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
2984 $logger->info("checking possibility of volume hold for volume ".$vol->id);
2986 my $filter_copies = [];
2987 for my $copy (@$copies) {
2988 # ignore part-mapped copies for regular volume level holds
2989 push(@$filter_copies, $copy) unless
2990 new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
2992 $copies = $filter_copies;
2997 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2998 "payload" => {"fail_part" => "no_ultimate_items"}
3004 my $age_protect_only = 0;
3005 for my $copy ( @$copies ) {
3006 @status = verify_copy_for_hold(
3007 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
3008 $age_protect_only ||= $status[3];
3011 $status[3] = $age_protect_only;
3017 sub verify_copy_for_hold {
3018 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
3019 # $oargs should be undef unless we're overriding.
3020 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
3021 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3024 requestor => $requestor,
3027 title_descriptor => $title->fixed_fields,
3028 pickup_lib => $pickup_lib,
3029 request_lib => $request_lib,
3031 show_event_list => 1
3035 # Check for override permissions on events.
3036 if ($oargs && $permitted && scalar @$permitted) {
3037 # Remove the events from permitted that we can override.
3038 if ($oargs->{events}) {
3039 foreach my $evt (@{$oargs->{events}}) {
3040 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3043 # Now, we handle the override all case by checking remaining
3044 # events against override permissions.
3045 if (scalar @$permitted && $oargs->{all}) {
3046 # Pre-set events and failed members of oargs to empty
3047 # arrays, if they are not set, yet.
3048 $oargs->{events} = [] unless ($oargs->{events});
3049 $oargs->{failed} = [] unless ($oargs->{failed});
3050 # When we're done with these checks, we swap permitted
3051 # with a reference to @disallowed.
3052 my @disallowed = ();
3053 foreach my $evt (@{$permitted}) {
3054 # Check if we've already seen the event in this
3055 # session and it failed.
3056 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3057 push(@disallowed, $evt);
3059 # We have to check if the requestor has the
3060 # override permission.
3062 # AppUtils::check_user_perms returns the perm if
3063 # the user doesn't have it, undef if they do.
3064 if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3065 push(@disallowed, $evt);
3066 push(@{$oargs->{failed}}, $evt->{textcode});
3068 push(@{$oargs->{events}}, $evt->{textcode});
3072 $permitted = \@disallowed;
3076 my $age_protect_only = 0;
3077 if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3078 $age_protect_only = 1;
3082 (not scalar @$permitted), # true if permitted is an empty arrayref
3083 ( # XXX This test is of very dubious value; someone should figure
3084 # out what if anything is checking this value
3085 ($copy->circ_lib == $pickup_lib) and
3086 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3095 sub find_nearest_permitted_hold {
3098 my $editor = shift; # CStoreEditor object
3099 my $copy = shift; # copy to target
3100 my $user = shift; # staff
3101 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3103 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3105 my $bc = $copy->barcode;
3107 # find any existing holds that already target this copy
3108 my $old_holds = $editor->search_action_hold_request(
3109 { current_copy => $copy->id,
3110 cancel_time => undef,
3111 capture_time => undef
3115 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3117 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3118 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3120 my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3122 # the nearest_hold API call now needs this
3123 $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3124 unless ref $copy->call_number;
3126 # search for what should be the best holds for this copy to fulfill
3127 my $best_holds = $U->storagereq(
3128 "open-ils.storage.action.hold_request.nearest_hold.atomic",
3129 $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3131 # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3133 for my $holdid (@$old_holds) {
3134 next unless $holdid;
3135 push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3139 unless(@$best_holds) {
3140 $logger->info("circulator: no suitable holds found for copy $bc");
3141 return (undef, $evt);
3147 # for each potential hold, we have to run the permit script
3148 # to make sure the hold is actually permitted.
3151 for my $holdid (@$best_holds) {
3152 next unless $holdid;
3153 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3155 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3156 # Force and recall holds bypass all rules
3157 if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3161 my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3162 my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3164 $reqr_cache{$hold->requestor} = $reqr;
3165 $org_cache{$hold->request_lib} = $rlib;
3167 # see if this hold is permitted
3168 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3170 patron_id => $hold->usr,
3173 pickup_lib => $hold->pickup_lib,
3174 request_lib => $rlib,
3186 unless( $best_hold ) { # no "good" permitted holds were found
3188 $logger->info("circulator: no suitable holds found for copy $bc");
3189 return (undef, $evt);
3192 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3194 # indicate a permitted hold was found
3195 return $best_hold if $check_only;
3197 # we've found a permitted hold. we need to "grab" the copy
3198 # to prevent re-targeted holds (next part) from re-grabbing the copy
3199 $best_hold->current_copy($copy->id);
3200 $editor->update_action_hold_request($best_hold)
3201 or return (undef, $editor->event);
3206 # re-target any other holds that already target this copy
3207 for my $old_hold (@$old_holds) {
3208 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3209 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3210 $old_hold->id." after a better hold [".$best_hold->id."] was found");
3211 $old_hold->clear_current_copy;
3212 $old_hold->clear_prev_check_time;
3213 $editor->update_action_hold_request($old_hold)
3214 or return (undef, $editor->event);
3215 push(@retarget, $old_hold->id);
3218 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3226 __PACKAGE__->register_method(
3227 method => 'all_rec_holds',
3228 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3232 my( $self, $conn, $auth, $title_id, $args ) = @_;
3234 my $e = new_editor(authtoken=>$auth);
3235 $e->checkauth or return $e->event;
3236 $e->allowed('VIEW_HOLD') or return $e->event;
3239 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
3240 $args->{cancel_time} = undef;
3243 metarecord_holds => []
3245 , volume_holds => []
3247 , recall_holds => []
3250 , issuance_holds => []
3253 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3255 $resp->{metarecord_holds} = $e->search_action_hold_request(
3256 { hold_type => OILS_HOLD_TYPE_METARECORD,
3257 target => $mr_map->metarecord,
3263 $resp->{title_holds} = $e->search_action_hold_request(
3265 hold_type => OILS_HOLD_TYPE_TITLE,
3266 target => $title_id,
3270 my $parts = $e->search_biblio_monograph_part(
3276 $resp->{part_holds} = $e->search_action_hold_request(
3278 hold_type => OILS_HOLD_TYPE_MONOPART,
3284 my $subs = $e->search_serial_subscription(
3285 { record_entry => $title_id }, {idlist=>1});
3288 my $issuances = $e->search_serial_issuance(
3289 {subscription => $subs}, {idlist=>1}
3293 $resp->{issuance_holds} = $e->search_action_hold_request(
3295 hold_type => OILS_HOLD_TYPE_ISSUANCE,
3296 target => $issuances,
3303 my $vols = $e->search_asset_call_number(
3304 { record => $title_id, deleted => 'f' }, {idlist=>1});
3306 return $resp unless @$vols;
3308 $resp->{volume_holds} = $e->search_action_hold_request(
3310 hold_type => OILS_HOLD_TYPE_VOLUME,
3315 my $copies = $e->search_asset_copy(
3316 { call_number => $vols, deleted => 'f' }, {idlist=>1});
3318 return $resp unless @$copies;
3320 $resp->{copy_holds} = $e->search_action_hold_request(
3322 hold_type => OILS_HOLD_TYPE_COPY,
3327 $resp->{recall_holds} = $e->search_action_hold_request(
3329 hold_type => OILS_HOLD_TYPE_RECALL,
3334 $resp->{force_holds} = $e->search_action_hold_request(
3336 hold_type => OILS_HOLD_TYPE_FORCE,
3348 __PACKAGE__->register_method(
3349 method => 'uber_hold',
3351 api_name => 'open-ils.circ.hold.details.retrieve'
3355 my($self, $client, $auth, $hold_id, $args) = @_;
3356 my $e = new_editor(authtoken=>$auth);
3357 $e->checkauth or return $e->event;
3358 return uber_hold_impl($e, $hold_id, $args);
3361 __PACKAGE__->register_method(
3362 method => 'batch_uber_hold',
3365 api_name => 'open-ils.circ.hold.details.batch.retrieve'
3368 sub batch_uber_hold {
3369 my($self, $client, $auth, $hold_ids, $args) = @_;
3370 my $e = new_editor(authtoken=>$auth);
3371 $e->checkauth or return $e->event;
3372 $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3376 sub uber_hold_impl {
3377 my($e, $hold_id, $args) = @_;
3380 my $hold = $e->retrieve_action_hold_request(
3385 flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
3388 ) or return $e->event;
3390 if($hold->usr->id ne $e->requestor->id) {
3391 # caller is asking for someone else's hold
3392 $e->allowed('VIEW_HOLD') or return $e->event;
3393 $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3394 [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3397 # caller is asking for own hold, but may not have permission to view staff notes
3398 unless($e->allowed('VIEW_HOLD')) {
3399 $hold->notes( # filter out any staff notes (unless marked as public)
3400 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3404 my $user = $hold->usr;
3405 $hold->usr($user->id);
3408 my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args->{suppress_mvr});
3410 flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3411 flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3413 my $details = retrieve_hold_queue_status_impl($e, $hold);
3418 ($copy ? (copy => $copy) : ()),
3419 ($volume ? (volume => $volume) : ()),
3420 ($issuance ? (issuance => $issuance) : ()),
3421 ($part ? (part => $part) : ()),
3422 ($args->{include_bre} ? (bre => $bre) : ()),
3423 ($args->{suppress_mvr} ? () : (mvr => $mvr)),
3427 $resp->{copy}->location(
3428 $e->retrieve_asset_copy_location($resp->{copy}->location))
3429 if $resp->{copy} and $args->{flesh_acpl};
3431 unless($args->{suppress_patron_details}) {
3432 my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3433 $resp->{patron_first} = $user->first_given_name,
3434 $resp->{patron_last} = $user->family_name,
3435 $resp->{patron_barcode} = $card->barcode,
3436 $resp->{patron_alias} = $user->alias,
3444 # -----------------------------------------------------
3445 # Returns the MVR object that represents what the
3447 # -----------------------------------------------------
3449 my( $e, $hold, $no_mvr ) = @_;
3457 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3458 my $mr = $e->retrieve_metabib_metarecord($hold->target)
3459 or return $e->event;
3460 $tid = $mr->master_record;
3462 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3463 $tid = $hold->target;
3465 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3466 $volume = $e->retrieve_asset_call_number($hold->target)
3467 or return $e->event;
3468 $tid = $volume->record;
3470 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3471 $issuance = $e->retrieve_serial_issuance([
3473 {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3474 ]) or return $e->event;
3476 $tid = $issuance->subscription->record_entry;
3478 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3479 $part = $e->retrieve_biblio_monograph_part([
3481 ]) or return $e->event;
3483 $tid = $part->record;
3485 } 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 ) {
3486 $copy = $e->retrieve_asset_copy([
3488 {flesh => 1, flesh_fields => {acp => ['call_number']}}
3489 ]) or return $e->event;
3491 $volume = $copy->call_number;
3492 $tid = $volume->record;
3495 if(!$copy and ref $hold->current_copy ) {
3496 $copy = $hold->current_copy;
3497 $hold->current_copy($copy->id);
3500 if(!$volume and $copy) {
3501 $volume = $e->retrieve_asset_call_number($copy->call_number);
3504 # TODO return metarcord mvr for M holds
3505 my $title = $e->retrieve_biblio_record_entry($tid);
3506 return ( ($no_mvr) ? undef : $U->record_to_mvr($title), $volume, $copy, $issuance, $part, $title );
3509 __PACKAGE__->register_method(
3510 method => 'clear_shelf_cache',
3511 api_name => 'open-ils.circ.hold.clear_shelf.get_cache',
3515 Returns the holds processed with the given cache key
3520 sub clear_shelf_cache {
3521 my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3522 my $e = new_editor(authtoken => $auth, xact => 1);
3523 return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3526 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3528 my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3531 $logger->info("no hold data found in cache"); # XXX TODO return event
3537 foreach (keys %$hold_data) {
3538 $maximum += scalar(@{ $hold_data->{$_} });
3540 $client->respond({"maximum" => $maximum, "progress" => 0});
3542 for my $action (sort keys %$hold_data) {
3543 while (@{$hold_data->{$action}}) {
3544 my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3546 my $result_chunk = $e->json_query({
3548 "acp" => ["barcode"],
3550 first_given_name second_given_name family_name alias
3553 "acnp" => [{column => "label", alias => "prefix"}],
3554 "acns" => [{column => "label", alias => "suffix"}],
3562 "field" => "id", "fkey" => "current_copy",
3565 "field" => "id", "fkey" => "call_number",
3568 "field" => "id", "fkey" => "record"
3571 "field" => "id", "fkey" => "prefix"
3574 "field" => "id", "fkey" => "suffix"
3578 "acpl" => {"field" => "id", "fkey" => "location"}
3581 "au" => {"field" => "id", "fkey" => "usr"}
3584 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3585 }, {"substream" => 1}) or return $e->die_event;
3589 +{"action" => $action, "hold_details" => $_}
3600 __PACKAGE__->register_method(
3601 method => 'clear_shelf_process',
3603 api_name => 'open-ils.circ.hold.clear_shelf.process',
3606 1. Find all holds that have expired on the holds shelf
3608 3. If a clear-shelf status is configured, put targeted copies into this status
3609 4. Divide copies into 3 groups: items to transit, items to reshelve, and items
3610 that are needed for holds. No subsequent action is taken on the holds
3611 or items after grouping.
3616 sub clear_shelf_process {
3617 my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
3619 my $e = new_editor(authtoken=>$auth);
3620 $e->checkauth or return $e->die_event;
3621 my $cache = OpenSRF::Utils::Cache->new('global');
3623 $org_id ||= $e->requestor->ws_ou;
3624 $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
3626 my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
3628 my @hold_ids = $self->method_lookup(
3629 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
3630 )->run($auth, $org_id, $match_copy);
3635 my @canceled_holds; # newly canceled holds
3636 $chunk_size ||= 25; # chunked status updates
3637 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3640 for my $hold_id (@hold_ids) {
3642 $logger->info("Clear shelf processing hold $hold_id");
3644 my $hold = $e->retrieve_action_hold_request([
3647 flesh_fields => {ahr => ['current_copy']}
3651 if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
3652 $hold->cancel_time('now');
3653 $hold->cancel_cause(2); # Hold Shelf expiration
3654 $e->update_action_hold_request($hold) or return $e->die_event;
3655 push(@canceled_holds, $hold_id);
3658 my $copy = $hold->current_copy;
3660 if($copy_status or $copy_status == 0) {
3661 # if a clear-shelf copy status is defined, update the copy
3662 $copy->status($copy_status);
3663 $copy->edit_date('now');
3664 $copy->editor($e->requestor->id);
3665 $e->update_asset_copy($copy) or return $e->die_event;
3668 push(@holds, $hold);
3669 $client->respond({maximum => scalar(@holds), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
3678 pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
3681 for my $hold (@holds) {
3683 my $copy = $hold->current_copy;
3684 my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
3686 if($alt_hold and !$match_copy) {
3688 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
3690 } elsif($copy->circ_lib != $e->requestor->ws_ou) {
3692 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
3696 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
3700 my $cache_key = md5_hex(time . $$ . rand());
3701 $logger->info("clear_shelf_cache: storing under $cache_key");
3702 $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours. configurable?
3704 # tell the client we're done
3705 $client->respond_complete({cache_key => $cache_key});
3708 # fire off the hold cancelation trigger and wait for response so don't flood the service
3710 # refetch the holds to pick up the caclulated cancel_time,
3711 # which may be needed by Action/Trigger
3713 my $updated_holds = [];
3714 $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
3717 $U->create_events_for_hook(
3718 'hold_request.cancel.expire_holds_shelf',
3719 $_, $org_id, undef, undef, 1) for @$updated_holds;
3722 # tell the client we're done
3723 $client->respond_complete;
3727 # returns IDs for holds that are on the holds shelf but
3728 # have had their pickup_libs change while on the shelf.
3729 sub pickup_lib_changed_on_shelf_holds {
3732 my $ignore_holds = shift;
3733 $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
3736 select => { alhr => ['id'] },
3741 fkey => 'current_copy'
3746 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
3748 capture_time => { "!=" => undef },
3749 fulfillment_time => undef,
3750 current_shelf_lib => $org_id,
3751 pickup_lib => {'!=' => {'+alhr' => 'current_shelf_lib'}}
3756 $query->{where}->{'+alhr'}->{id} =
3757 {'not in' => $ignore_holds} if @$ignore_holds;
3759 my $hold_ids = $e->json_query($query);
3760 return [ map { $_->{id} } @$hold_ids ];
3763 __PACKAGE__->register_method(
3764 method => 'usr_hold_summary',
3765 api_name => 'open-ils.circ.holds.user_summary',
3767 Returns a summary of holds statuses for a given user
3771 sub usr_hold_summary {
3772 my($self, $conn, $auth, $user_id) = @_;
3774 my $e = new_editor(authtoken=>$auth);
3775 $e->checkauth or return $e->event;
3776 $e->allowed('VIEW_HOLD') or return $e->event;
3778 my $holds = $e->search_action_hold_request(
3781 fulfillment_time => undef,
3782 cancel_time => undef,
3786 my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
3787 $summary{_hold_status($e, $_)} += 1 for @$holds;
3793 __PACKAGE__->register_method(
3794 method => 'hold_has_copy_at',
3795 api_name => 'open-ils.circ.hold.has_copy_at',
3798 'Returns the ID of the found copy and name of the shelving location if there is ' .
3799 'an available copy at the specified org unit. Returns empty hash otherwise. ' .
3800 'The anticipated use for this method is to determine whether an item is ' .
3801 'available at the library where the user is placing the hold (or, alternatively, '.
3802 'at the pickup library) to encourage bypassing the hold placement and just ' .
3803 'checking out the item.' ,
3805 { desc => 'Authentication Token', type => 'string' },
3806 { desc => 'Method Arguments. Options include: hold_type, hold_target, org_unit. '
3807 . 'hold_type is the hold type code (T, V, C, M, ...). '
3808 . 'hold_target is the identifier of the hold target object. '
3809 . 'org_unit is org unit ID.',
3814 desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
3820 sub hold_has_copy_at {
3821 my($self, $conn, $auth, $args) = @_;
3823 my $e = new_editor(authtoken=>$auth);
3824 $e->checkauth or return $e->event;
3826 my $hold_type = $$args{hold_type};
3827 my $hold_target = $$args{hold_target};
3828 my $org_unit = $$args{org_unit};
3831 select => {acp => ['id'], acpl => ['name']},
3836 filter => { holdable => 't', deleted => 'f' },
3839 ccs => {field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3842 where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit, status => [0,7]}},
3846 if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
3848 $query->{where}->{'+acp'}->{id} = $hold_target;
3850 } elsif($hold_type eq 'V') {
3852 $query->{where}->{'+acp'}->{call_number} = $hold_target;
3854 } elsif($hold_type eq 'P') {
3856 $query->{from}->{acp}->{acpm} = {
3857 field => 'target_copy',
3859 filter => {part => $hold_target},
3862 } elsif($hold_type eq 'I') {
3864 $query->{from}->{acp}->{sitem} = {
3867 filter => {issuance => $hold_target},
3870 } elsif($hold_type eq 'T') {
3872 $query->{from}->{acp}->{acn} = {
3874 fkey => 'call_number',
3878 filter => {id => $hold_target},
3886 $query->{from}->{acp}->{acn} = {
3888 fkey => 'call_number',
3897 filter => {metarecord => $hold_target},
3905 my $res = $e->json_query($query)->[0] or return {};
3906 return {copy => $res->{id}, location => $res->{name}} if $res;
3910 # returns true if the user already has an item checked out
3911 # that could be used to fulfill the requested hold.
3912 sub hold_item_is_checked_out {
3913 my($e, $user_id, $hold_type, $hold_target) = @_;
3916 select => {acp => ['id']},
3917 from => {acp => {}},
3921 in => { # copies for circs the user has checked out
3922 select => {circ => ['target_copy']},
3926 checkin_time => undef,
3928 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
3929 {stop_fines => undef}
3939 if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
3941 $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
3943 } elsif($hold_type eq 'V') {
3945 $query->{where}->{'+acp'}->{call_number} = $hold_target;
3947 } elsif($hold_type eq 'P') {
3949 $query->{from}->{acp}->{acpm} = {
3950 field => 'target_copy',
3952 filter => {part => $hold_target},
3955 } elsif($hold_type eq 'I') {
3957 $query->{from}->{acp}->{sitem} = {
3960 filter => {issuance => $hold_target},
3963 } elsif($hold_type eq 'T') {
3965 $query->{from}->{acp}->{acn} = {
3967 fkey => 'call_number',
3971 filter => {id => $hold_target},
3979 $query->{from}->{acp}->{acn} = {
3981 fkey => 'call_number',
3990 filter => {metarecord => $hold_target},
3998 return $e->json_query($query)->[0];
4001 __PACKAGE__->register_method(
4002 method => 'change_hold_title',
4003 api_name => 'open-ils.circ.hold.change_title',
4006 Updates all title level holds targeting the specified bibs to point a new bib./,
4008 { desc => 'Authentication Token', type => 'string' },
4009 { desc => 'New Target Bib Id', type => 'number' },
4010 { desc => 'Old Target Bib Ids', type => 'array' },
4012 return => { desc => '1 on success' }
4016 __PACKAGE__->register_method(
4017 method => 'change_hold_title_for_specific_holds',
4018 api_name => 'open-ils.circ.hold.change_title.specific_holds',
4021 Updates specified holds to target new bib./,
4023 { desc => 'Authentication Token', type => 'string' },
4024 { desc => 'New Target Bib Id', type => 'number' },
4025 { desc => 'Holds Ids for holds to update', type => 'array' },
4027 return => { desc => '1 on success' }
4032 sub change_hold_title {
4033 my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4035 my $e = new_editor(authtoken=>$auth, xact=>1);
4036 return $e->die_event unless $e->checkauth;
4038 my $holds = $e->search_action_hold_request(
4041 capture_time => undef,
4042 cancel_time => undef,
4043 fulfillment_time => undef,
4049 flesh_fields => { ahr => ['usr'] }
4055 for my $hold (@$holds) {
4056 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4057 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4058 $hold->target( $new_bib_id );
4059 $e->update_action_hold_request($hold) or return $e->die_event;
4064 _reset_hold($self, $e->requestor, $_) for @$holds;
4069 sub change_hold_title_for_specific_holds {
4070 my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4072 my $e = new_editor(authtoken=>$auth, xact=>1);
4073 return $e->die_event unless $e->checkauth;
4075 my $holds = $e->search_action_hold_request(
4078 capture_time => undef,
4079 cancel_time => undef,
4080 fulfillment_time => undef,
4086 flesh_fields => { ahr => ['usr'] }
4092 for my $hold (@$holds) {
4093 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4094 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4095 $hold->target( $new_bib_id );
4096 $e->update_action_hold_request($hold) or return $e->die_event;
4101 _reset_hold($self, $e->requestor, $_) for @$holds;
4106 __PACKAGE__->register_method(
4107 method => 'rec_hold_count',
4108 api_name => 'open-ils.circ.bre.holds.count',
4110 desc => q/Returns the total number of holds that target the
4111 selected bib record or its associated copies and call_numbers/,
4113 { desc => 'Bib ID', type => 'number' },
4114 { desc => q/Optional arguments. Supported arguments include:
4115 "pickup_lib_descendant" -> limit holds to those whose pickup
4116 library is equal to or is a child of the provided org unit/,
4120 return => {desc => 'Hold count', type => 'number'}
4124 __PACKAGE__->register_method(
4125 method => 'rec_hold_count',
4126 api_name => 'open-ils.circ.mmr.holds.count',
4128 desc => q/Returns the total number of holds that target the
4129 selected metarecord or its associated copies, call_numbers, and bib records/,
4131 { desc => 'Metarecord ID', type => 'number' },
4133 return => {desc => 'Hold count', type => 'number'}
4137 # XXX Need to add type I holds to these counts
4138 sub rec_hold_count {
4139 my($self, $conn, $target_id, $args) = @_;
4146 filter => {metarecord => $target_id}
4153 filter => { id => $target_id },
4158 if($self->api_name =~ /mmr/) {
4159 delete $bre_join->{bre}->{filter};
4160 $bre_join->{bre}->{join} = $mmr_join;
4166 fkey => 'call_number',
4172 select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4176 cancel_time => undef,
4177 fulfillment_time => undef,
4181 hold_type => [qw/C F R/],
4184 select => {acp => ['id']},
4185 from => { acp => $cn_join }
4195 select => {acn => ['id']},
4196 from => {acn => $bre_join}
4206 select => {bmp => ['id']},
4207 from => {bmp => $bre_join}
4215 target => $target_id
4223 if($self->api_name =~ /mmr/) {
4224 $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4229 select => {bre => ['id']},
4230 from => {bre => $mmr_join}
4236 $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4239 target => $target_id
4245 if (my $pld = $args->{pickup_lib_descendant}) {
4247 my $top_ou = new_editor()->search_actor_org_unit(
4248 {parent_ou => undef}
4249 )->[0]; # XXX Assumes single root node. Not alone in this...
4251 $query->{where}->{'+ahr'}->{pickup_lib} = {
4253 select => {aou => [{
4255 transform => 'actor.org_unit_descendants',
4256 result_field => 'id'
4259 where => {id => $pld}
4261 } if ($pld != $top_ou->id);
4265 return new_editor()->json_query($query)->[0]->{count};
4268 # A helper function to calculate a hold's expiration time at a given
4269 # org_unit. Takes the org_unit as an argument and returns either the
4270 # hold expire time as an ISO8601 string or undef if there is no hold
4271 # expiration interval set for the subject ou.
4272 sub calculate_expire_time
4275 my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4277 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
4278 return $U->epoch2ISO8601($date->epoch);
4284 __PACKAGE__->register_method(
4285 method => 'mr_hold_filter_attrs',
4286 api_name => 'open-ils.circ.mmr.holds.filters',
4291 Returns the set of available formats and languages for the
4292 constituent records of the provided metarcord.
4293 If an array of hold IDs is also provided, information about
4294 each is returned as well. This information includes:
4295 1. a slightly easier to read version of holdable_formats
4296 2. attributes describing the set of format icons included
4297 in the set of desired, constituent records.
4300 {desc => 'Metarecord ID', type => 'number'},
4301 {desc => 'Context Org ID', type => 'number'},
4302 {desc => 'Hold ID List', type => 'array'},
4306 Stream of objects. The first will have a 'metarecord' key
4307 containing non-hold-specific metarecord information, subsequent
4308 responses will contain a 'hold' key containing hold-specific
4316 sub mr_hold_filter_attrs {
4317 my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4318 my $e = new_editor();
4320 # by default, return MR / hold attributes for all constituent
4321 # records with holdable copies. If there is a hard boundary,
4322 # though, limit to records with copies within the boundary,
4323 # since anything outside the boundary can never be held.
4326 $org_depth = $U->ou_ancestor_setting_value(
4327 $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4330 # get all org-scoped records w/ holdable copies for this metarecord
4331 my ($bre_ids) = $self->method_lookup(
4332 'open-ils.circ.holds.metarecord.filtered_records')->run(
4333 $mr_id, undef, $org_id, $org_depth);
4335 my $item_lang_attr = 'item_lang'; # configurable?
4336 my $format_attr = $e->retrieve_config_global_flag(
4337 'opac.metarecord.holds.format_attr')->value;
4339 # helper sub for fetching ccvms for a batch of record IDs
4340 sub get_batch_ccvms {
4341 my ($e, $attr, $bre_ids) = @_;
4342 return [] unless $bre_ids and @$bre_ids;
4343 my $vals = $e->search_metabib_record_attr_flat({
4347 return [] unless @$vals;
4348 return $e->search_config_coded_value_map({
4350 code => [map {$_->value} @$vals]
4354 my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4355 my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4360 formats => $formats,
4365 return unless $hold_ids;
4366 my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4367 $icon_attr = $icon_attr ? $icon_attr->value : '';
4369 for my $hold_id (@$hold_ids) {
4370 my $hold = $e->retrieve_action_hold_request($hold_id)
4371 or return $e->event;
4373 next unless $hold->hold_type eq 'M';
4383 # collect the ccvm's for the selected formats / language
4384 # (i.e. the holdable formats) on the MR.
4385 # this assumes a two-key structure for format / language,
4386 # though no assumption is made about the keys themselves.
4387 my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4389 my $format_vals = [];
4390 for my $val (values %$hformats) {
4391 # val is either a single ccvm or an array of them
4392 $val = [$val] unless ref $val eq 'ARRAY';
4393 for my $node (@$val) {
4394 push (@$lang_vals, $node->{_val})
4395 if $node->{_attr} eq $item_lang_attr;
4396 push (@$format_vals, $node->{_val})
4397 if $node->{_attr} eq $format_attr;
4401 # fetch the ccvm's for consistency with the {metarecord} blob
4402 $resp->{hold}{formats} = $e->search_config_coded_value_map({
4403 ctype => $format_attr, code => $format_vals});
4404 $resp->{hold}{langs} = $e->search_config_coded_value_map({
4405 ctype => $item_lang_attr, code => $lang_vals});
4407 # find all of the bib records within this metarcord whose
4408 # format / language match the holdable formats on the hold
4409 my ($bre_ids) = $self->method_lookup(
4410 'open-ils.circ.holds.metarecord.filtered_records')->run(
4411 $hold->target, $hold->holdable_formats,
4412 $hold->selection_ou, $hold->selection_depth);
4414 # now find all of the 'icon' attributes for the records
4415 $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4416 $client->respond($resp);
4422 __PACKAGE__->register_method(
4423 method => "copy_has_holds_count",
4424 api_name => "open-ils.circ.copy.has_holds_count",
4428 Returns the number of holds a paticular copy has
4431 { desc => 'Authentication Token', type => 'string'},
4432 { desc => 'Copy ID', type => 'number'}
4443 sub copy_has_holds_count {
4444 my( $self, $conn, $auth, $copyid ) = @_;
4445 my $e = new_editor(authtoken=>$auth);
4446 return $e->event unless $e->checkauth;
4448 if( $copyid && $copyid > 0 ) {
4449 my $meth = 'retrieve_action_has_holds_count';
4450 my $data = $e->$meth($copyid);
4452 return $data->count();