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);
384 'open-ils.storage.action.hold_request.copy_targeter',
385 undef, $hold->id ) 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([
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->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $hold_id);
755 __PACKAGE__->register_method(
756 method => "cancel_hold",
757 api_name => "open-ils.circ.hold.cancel",
759 desc => 'Cancels the specified hold. The login session is the requestor. If the requestor is different from the usr field ' .
760 'on the hold, the requestor must have CANCEL_HOLDS permissions. The hold may be either the hold object or the hold id',
762 {desc => 'Authentication token', type => 'string'},
763 {desc => 'Hold ID', type => 'number'},
764 {desc => 'Cause of Cancellation', type => 'string'},
765 {desc => 'Note', type => 'string'}
768 desc => '1 on success, event on error'
774 my($self, $client, $auth, $holdid, $cause, $note) = @_;
776 my $e = new_editor(authtoken=>$auth, xact=>1);
777 return $e->die_event unless $e->checkauth;
779 my $hold = $e->retrieve_action_hold_request($holdid)
780 or return $e->die_event;
782 if( $e->requestor->id ne $hold->usr ) {
783 return $e->die_event unless $e->allowed('CANCEL_HOLDS');
786 if ($hold->cancel_time) {
791 # If the hold is captured, reset the copy status
792 if( $hold->capture_time and $hold->current_copy ) {
794 my $copy = $e->retrieve_asset_copy($hold->current_copy)
795 or return $e->die_event;
797 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
798 $logger->info("canceling hold $holdid whose item is on the holds shelf");
799 # $logger->info("setting copy to status 'reshelving' on hold cancel");
800 # $copy->status(OILS_COPY_STATUS_RESHELVING);
801 # $copy->editor($e->requestor->id);
802 # $copy->edit_date('now');
803 # $e->update_asset_copy($copy) or return $e->event;
805 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
808 $logger->warn("! canceling hold [$hid] that is in transit");
809 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
812 my $trans = $e->retrieve_action_transit_copy($transid);
813 # Leave the transit alive, but set the copy status to
814 # reshelving so it will be properly reshelved when it gets back home
816 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
817 $e->update_action_transit_copy($trans) or return $e->die_event;
823 $hold->cancel_time('now');
824 $hold->cancel_cause($cause);
825 $hold->cancel_note($note);
826 $e->update_action_hold_request($hold)
827 or return $e->die_event;
831 # re-fetch the hold to pick up the real cancel_time (not "now") for A/T
833 $hold = $e->retrieve_action_hold_request($hold->id) or return $e->die_event;
836 if ($e->requestor->id == $hold->usr) {
837 $U->create_events_for_hook('hold_request.cancel.patron', $hold, $hold->pickup_lib);
839 $U->create_events_for_hook('hold_request.cancel.staff', $hold, $hold->pickup_lib);
845 my $update_hold_desc = 'The login session is the requestor. ' .
846 'If the requestor is different from the usr field on the hold, ' .
847 'the requestor must have UPDATE_HOLDS permissions. ' .
848 'If supplying a hash of hold data, "id" must be included. ' .
849 'The hash is ignored if a hold object is supplied, ' .
850 'so you should supply only one kind of hold data argument.' ;
852 __PACKAGE__->register_method(
853 method => "update_hold",
854 api_name => "open-ils.circ.hold.update",
856 desc => "Updates the specified hold. $update_hold_desc",
858 {desc => 'Authentication token', type => 'string'},
859 {desc => 'Hold Object', type => 'object'},
860 {desc => 'Hash of values to be applied', type => 'object'}
863 desc => 'Hold ID on success, event on error',
869 __PACKAGE__->register_method(
870 method => "batch_update_hold",
871 api_name => "open-ils.circ.hold.update.batch",
874 desc => "Updates the specified hold(s). $update_hold_desc",
876 {desc => 'Authentication token', type => 'string'},
877 {desc => 'Array of hold obejcts', type => 'array' },
878 {desc => 'Array of hashes of values to be applied', type => 'array' }
881 desc => 'Hold ID per success, event per error',
887 my($self, $client, $auth, $hold, $values) = @_;
888 my $e = new_editor(authtoken=>$auth, xact=>1);
889 return $e->die_event unless $e->checkauth;
890 my $resp = update_hold_impl($self, $e, $hold, $values);
891 if ($U->event_code($resp)) {
895 $e->commit; # FIXME: update_hold_impl already does $e->commit ??
899 sub batch_update_hold {
900 my($self, $client, $auth, $hold_list, $values_list) = @_;
901 my $e = new_editor(authtoken=>$auth);
902 return $e->die_event unless $e->checkauth;
904 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.
906 $values_list ||= []; # FIXME: either move this above $count declaration, or send an event if both lists undef. Probably the latter.
908 # FIXME: Failing over to [] guarantees warnings for "Use of unitialized value" in update_hold_impl call.
909 # FIXME: We should be sure we only call update_hold_impl with hold object OR hash, not both.
911 for my $idx (0..$count-1) {
913 my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
914 $e->xact_commit unless $U->event_code($resp);
915 $client->respond($resp);
919 return undef; # not in the register return type, assuming we should always have at least one list populated
922 sub update_hold_impl {
923 my($self, $e, $hold, $values) = @_;
925 my $need_retarget = 0;
928 $hold = $e->retrieve_action_hold_request($values->{id})
929 or return $e->die_event;
930 for my $k (keys %$values) {
931 # Outside of pickup_lib (covered by the first regex) I don't know when these would currently change.
932 # But hey, why not cover things that may happen later?
933 if ($k =~ '_(lib|ou)$' || $k eq 'target' || $k eq 'hold_type' || $k eq 'requestor' || $k eq 'selection_depth' || $k eq 'holdable_formats') {
934 if (defined $values->{$k} && defined $hold->$k() && $values->{$k} ne $hold->$k()) {
935 # Value changed? RETARGET!
937 } elsif (defined $hold->$k() != defined $values->{$k}) {
938 # Value being set or cleared? RETARGET!
942 if (defined $values->{$k}) {
943 $hold->$k($values->{$k});
945 my $f = "clear_$k"; $hold->$f();
950 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
951 or return $e->die_event;
953 # don't allow the user to be changed
954 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
956 if($hold->usr ne $e->requestor->id) {
957 # if the hold is for a different user, make sure the
958 # requestor has the appropriate permissions
959 my $usr = $e->retrieve_actor_user($hold->usr)
960 or return $e->die_event;
961 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
965 # --------------------------------------------------------------
966 # Changing the request time is like playing God
967 # --------------------------------------------------------------
968 if($hold->request_time ne $orig_hold->request_time) {
969 return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
970 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
974 # --------------------------------------------------------------
975 # Code for making sure staff have appropriate permissons for cut_in_line
976 # This, as is, doesn't prevent a user from cutting their own holds in line
978 # --------------------------------------------------------------
979 if($U->is_true($hold->cut_in_line) ne $U->is_true($orig_hold->cut_in_line)) {
980 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
984 # --------------------------------------------------------------
985 # Disallow hold suspencion if the hold is already captured.
986 # --------------------------------------------------------------
987 if ($U->is_true($hold->frozen) and not $U->is_true($orig_hold->frozen)) {
988 $hold_status = _hold_status($e, $hold);
989 if ($hold_status > 2 && $hold_status != 7) { # hold is captured
990 $logger->info("bypassing hold freeze on captured hold");
991 return OpenILS::Event->new('HOLD_SUSPEND_AFTER_CAPTURE');
996 # --------------------------------------------------------------
997 # if the hold is on the holds shelf or in transit and the pickup
998 # lib changes we need to create a new transit.
999 # --------------------------------------------------------------
1000 if($orig_hold->pickup_lib ne $hold->pickup_lib) {
1002 $hold_status = _hold_status($e, $hold) unless $hold_status;
1004 if($hold_status == 3) { # in transit
1006 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
1007 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
1009 $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
1011 # update the transit to reflect the new pickup location
1012 my $transit = $e->search_action_hold_transit_copy(
1013 {hold=>$hold->id, dest_recv_time => undef})->[0]
1014 or return $e->die_event;
1016 $transit->prev_dest($transit->dest); # mark the previous destination on the transit
1017 $transit->dest($hold->pickup_lib);
1018 $e->update_action_hold_transit_copy($transit) or return $e->die_event;
1020 } elsif($hold_status == 4 or $hold_status == 5 or $hold_status == 8) { # on holds shelf
1022 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
1023 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
1025 $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
1027 if ($hold->pickup_lib eq $orig_hold->current_shelf_lib) {
1028 # This can happen if the pickup lib is changed while the hold is
1029 # on the shelf, then changed back to the original pickup lib.
1030 # Restore the original shelf_expire_time to prevent abuse.
1031 set_hold_shelf_expire_time(undef, $hold, $e, $hold->shelf_time);
1034 # clear to prevent premature shelf expiration
1035 $hold->clear_shelf_expire_time;
1040 if($U->is_true($hold->frozen)) {
1041 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1042 $hold->clear_current_copy;
1043 $hold->clear_prev_check_time;
1044 # Clear expire_time to prevent frozen holds from expiring.
1045 $logger->info("clearing expire_time for frozen hold ".$hold->id);
1046 $hold->clear_expire_time;
1049 # If the hold_expire_time is in the past && is not equal to the
1050 # original expire_time, then reset the expire time to be in the
1052 if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1 && $U->datecmp($hold->expire_time, $orig_hold->expire_time) != 0) {
1053 $hold->expire_time(calculate_expire_time($hold->request_lib));
1056 # If the hold is reactivated, reset the expire_time.
1057 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1058 $logger->info("Reset expire_time on activated hold ".$hold->id);
1059 $hold->expire_time(calculate_expire_time($hold->request_lib));
1062 $e->update_action_hold_request($hold) or return $e->die_event;
1065 if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
1066 $logger->info("Running targeter on activated hold ".$hold->id);
1067 $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1070 # a change to mint-condition changes the set of potential copies, so retarget the hold;
1071 if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
1072 _reset_hold($self, $e->requestor, $hold)
1073 } elsif($need_retarget && !defined $hold->capture_time()) { # If needed, retarget the hold due to changes
1075 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1081 # this does not update the hold in the DB. It only
1082 # sets the shelf_expire_time field on the hold object.
1083 # start_time is optional and defaults to 'now'
1084 sub set_hold_shelf_expire_time {
1085 my ($class, $hold, $editor, $start_time) = @_;
1087 my $shelf_expire = $U->ou_ancestor_setting_value(
1089 'circ.holds.default_shelf_expire_interval',
1093 return undef unless $shelf_expire;
1095 $start_time = ($start_time) ?
1096 DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) :
1097 DateTime->now(time_zone => 'local'); # without time_zone we get UTC ... yuck!
1099 my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
1100 my $expire_time = $start_time->add(seconds => $seconds);
1102 # if the shelf expire time overlaps with a pickup lib's
1103 # closed date, push it out to the first open date
1104 my $dateinfo = $U->storagereq(
1105 'open-ils.storage.actor.org_unit.closed_date.overlap',
1106 $hold->pickup_lib, $expire_time->strftime('%FT%T%z'));
1109 my $dt_parser = DateTime::Format::ISO8601->new;
1110 $expire_time = $dt_parser->parse_datetime(cleanse_ISO8601($dateinfo->{end}));
1112 # TODO: enable/disable time bump via setting?
1113 $expire_time->set(hour => '23', minute => '59', second => '59');
1115 $logger->info("circulator: shelf_expire_time overlaps".
1116 " with closed date, pushing expire time to $expire_time");
1119 $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
1125 my($e, $orig_hold, $hold, $copy) = @_;
1126 my $src = $orig_hold->pickup_lib;
1127 my $dest = $hold->pickup_lib;
1129 $logger->info("putting hold into transit on pickup_lib update");
1131 my $transit = Fieldmapper::action::hold_transit_copy->new;
1132 $transit->hold($hold->id);
1133 $transit->source($src);
1134 $transit->dest($dest);
1135 $transit->target_copy($copy->id);
1136 $transit->source_send_time('now');
1137 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
1139 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
1140 $copy->editor($e->requestor->id);
1141 $copy->edit_date('now');
1143 $e->create_action_hold_transit_copy($transit) or return $e->die_event;
1144 $e->update_asset_copy($copy) or return $e->die_event;
1148 # if the hold is frozen, this method ensures that the hold is not "targeted",
1149 # that is, it clears the current_copy and prev_check_time to essentiallly
1150 # reset the hold. If it is being activated, it runs the targeter in the background
1151 sub update_hold_if_frozen {
1152 my($self, $e, $hold, $orig_hold) = @_;
1153 return if $hold->capture_time;
1155 if($U->is_true($hold->frozen)) {
1156 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
1157 $hold->clear_current_copy;
1158 $hold->clear_prev_check_time;
1161 if($U->is_true($orig_hold->frozen)) {
1162 $logger->info("Running targeter on activated hold ".$hold->id);
1163 $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1168 __PACKAGE__->register_method(
1169 method => "hold_note_CUD",
1170 api_name => "open-ils.circ.hold_request.note.cud",
1172 desc => 'Create, update or delete a hold request note. If the operator (from Auth. token) '
1173 . 'is not the owner of the hold, the UPDATE_HOLD permission is required',
1175 { desc => 'Authentication token', type => 'string' },
1176 { desc => 'Hold note object', type => 'object' }
1179 desc => 'Returns the note ID, event on error'
1185 my($self, $conn, $auth, $note) = @_;
1187 my $e = new_editor(authtoken => $auth, xact => 1);
1188 return $e->die_event unless $e->checkauth;
1190 my $hold = $e->retrieve_action_hold_request($note->hold)
1191 or return $e->die_event;
1193 if($hold->usr ne $e->requestor->id) {
1194 my $usr = $e->retrieve_actor_user($hold->usr);
1195 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
1196 $note->staff('t') if $note->isnew;
1200 $e->create_action_hold_request_note($note) or return $e->die_event;
1201 } elsif($note->ischanged) {
1202 $e->update_action_hold_request_note($note) or return $e->die_event;
1203 } elsif($note->isdeleted) {
1204 $e->delete_action_hold_request_note($note) or return $e->die_event;
1212 __PACKAGE__->register_method(
1213 method => "retrieve_hold_status",
1214 api_name => "open-ils.circ.hold.status.retrieve",
1216 desc => 'Calculates the current status of the hold. The requestor must have ' .
1217 'VIEW_HOLD permissions if the hold is for a user other than the requestor' ,
1219 { desc => 'Hold ID', type => 'number' }
1222 # type => 'number', # event sometimes
1223 desc => <<'END_OF_DESC'
1224 Returns event on error or:
1225 -1 on error (for now),
1226 1 for 'waiting for copy to become available',
1227 2 for 'waiting for copy capture',
1230 5 for 'hold-shelf-delay'
1233 8 for 'captured, on wrong hold shelf'
1240 sub retrieve_hold_status {
1241 my($self, $client, $auth, $hold_id) = @_;
1243 my $e = new_editor(authtoken => $auth);
1244 return $e->event unless $e->checkauth;
1245 my $hold = $e->retrieve_action_hold_request($hold_id)
1246 or return $e->event;
1248 if( $e->requestor->id != $hold->usr ) {
1249 return $e->event unless $e->allowed('VIEW_HOLD');
1252 return _hold_status($e, $hold);
1258 if ($hold->cancel_time) {
1261 if ($U->is_true($hold->frozen) && !$hold->capture_time) {
1264 if ($hold->current_shelf_lib and $hold->current_shelf_lib ne $hold->pickup_lib) {
1267 if ($hold->fulfillment_time) {
1270 return 1 unless $hold->current_copy;
1271 return 2 unless $hold->capture_time;
1273 my $copy = $hold->current_copy;
1274 unless( ref $copy ) {
1275 $copy = $e->retrieve_asset_copy($hold->current_copy)
1276 or return $e->event;
1279 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
1281 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
1283 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
1284 return 4 unless $hs_wait_interval;
1286 # if a hold_shelf_status_delay interval is defined and start_time plus
1287 # the interval is greater than now, consider the hold to be in the virtual
1288 # "on its way to the holds shelf" status. Return 5.
1290 my $transit = $e->search_action_hold_transit_copy({
1292 target_copy => $copy->id,
1293 dest_recv_time => {'!=' => undef},
1295 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
1296 $start_time = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time));
1297 my $end_time = $start_time->add(seconds => OpenSRF::Utils::interval_to_seconds($hs_wait_interval));
1299 return 5 if $end_time > DateTime->now;
1308 __PACKAGE__->register_method(
1309 method => "retrieve_hold_queue_stats",
1310 api_name => "open-ils.circ.hold.queue_stats.retrieve",
1312 desc => 'Returns summary data about the state of a hold',
1314 { desc => 'Authentication token', type => 'string'},
1315 { desc => 'Hold ID', type => 'number'},
1318 desc => q/Summary object with keys:
1319 total_holds : total holds in queue
1320 queue_position : current queue position
1321 potential_copies : number of potential copies for this hold
1322 estimated_wait : estimated wait time in days
1323 status : hold status
1324 -1 => error or unexpected state,
1325 1 => 'waiting for copy to become available',
1326 2 => 'waiting for copy capture',
1329 5 => 'hold-shelf-delay'
1336 sub retrieve_hold_queue_stats {
1337 my($self, $conn, $auth, $hold_id) = @_;
1338 my $e = new_editor(authtoken => $auth);
1339 return $e->event unless $e->checkauth;
1340 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
1341 if($e->requestor->id != $hold->usr) {
1342 return $e->event unless $e->allowed('VIEW_HOLD');
1344 return retrieve_hold_queue_status_impl($e, $hold);
1347 sub retrieve_hold_queue_status_impl {
1351 # The holds queue is defined as the distinct set of holds that share at
1352 # least one potential copy with the context hold, plus any holds that
1353 # share the same hold type and target. The latter part exists to
1354 # accomodate holds that currently have no potential copies
1355 my $q_holds = $e->json_query({
1357 # fetch cut_in_line and request_time since they're in the order_by
1358 # and we're asking for distinct values
1359 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1363 select => { ahcm => ['hold'] },
1368 'field' => 'target_copy',
1369 'fkey' => 'target_copy'
1373 where => { '+ahcm2' => { hold => $hold->id } },
1380 "field" => "cut_in_line",
1381 "transform" => "coalesce",
1383 "direction" => "desc"
1385 { "class" => "ahr", "field" => "request_time" }
1390 if (!@$q_holds) { # none? maybe we don't have a map ...
1391 $q_holds = $e->json_query({
1392 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1397 "field" => "cut_in_line",
1398 "transform" => "coalesce",
1400 "direction" => "desc"
1402 { "class" => "ahr", "field" => "request_time" }
1405 hold_type => $hold->hold_type,
1406 target => $hold->target,
1407 capture_time => undef,
1408 cancel_time => undef,
1410 {expire_time => undef },
1411 {expire_time => {'>' => 'now'}}
1419 for my $h (@$q_holds) {
1420 last if $h->{id} == $hold->id;
1424 my $hold_data = $e->json_query({
1426 acp => [ {column => 'id', transform => 'count', aggregate => 1, alias => 'count'} ],
1427 ccm => [ {column =>'avg_wait_time'} ]
1433 ccm => {type => 'left'}
1438 where => {'+ahcm' => {hold => $hold->id} }
1441 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1443 my $default_wait = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
1444 my $min_wait = $U->ou_ancestor_setting_value($user_org, 'circ.holds.min_estimated_wait_interval');
1445 $min_wait = OpenSRF::Utils::interval_to_seconds($min_wait || '0 seconds');
1446 $default_wait ||= '0 seconds';
1448 # Estimated wait time is the average wait time across the set
1449 # of potential copies, divided by the number of potential copies
1450 # times the queue position.
1452 my $combined_secs = 0;
1453 my $num_potentials = 0;
1455 for my $wait_data (@$hold_data) {
1456 my $count += $wait_data->{count};
1457 $combined_secs += $count *
1458 OpenSRF::Utils::interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
1459 $num_potentials += $count;
1462 my $estimated_wait = -1;
1464 if($num_potentials) {
1465 my $avg_wait = $combined_secs / $num_potentials;
1466 $estimated_wait = $qpos * ($avg_wait / $num_potentials);
1467 $estimated_wait = $min_wait if $estimated_wait < $min_wait and $estimated_wait != -1;
1471 total_holds => scalar(@$q_holds),
1472 queue_position => $qpos,
1473 potential_copies => $num_potentials,
1474 status => _hold_status( $e, $hold ),
1475 estimated_wait => int($estimated_wait)
1480 sub fetch_open_hold_by_current_copy {
1483 my $hold = $apputils->simplereq(
1485 'open-ils.cstore.direct.action.hold_request.search.atomic',
1486 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1487 return $hold->[0] if ref($hold);
1491 sub fetch_related_holds {
1494 return $apputils->simplereq(
1496 'open-ils.cstore.direct.action.hold_request.search.atomic',
1497 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1501 __PACKAGE__->register_method(
1502 method => "hold_pull_list",
1503 api_name => "open-ils.circ.hold_pull_list.retrieve",
1505 desc => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1506 'The location is determined by the login session.',
1508 { desc => 'Limit (optional)', type => 'number'},
1509 { desc => 'Offset (optional)', type => 'number'},
1512 desc => 'reference to a list of holds, or event on failure',
1517 __PACKAGE__->register_method(
1518 method => "hold_pull_list",
1519 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
1521 desc => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
1522 'The location is determined by the login session.',
1524 { desc => 'Limit (optional)', type => 'number'},
1525 { desc => 'Offset (optional)', type => 'number'},
1528 desc => 'reference to a list of holds, or event on failure',
1533 __PACKAGE__->register_method(
1534 method => "hold_pull_list",
1535 api_name => "open-ils.circ.hold_pull_list.retrieve.count",
1537 desc => 'Returns a count of holds that need to be "pulled" by a given location. ' .
1538 'The location is determined by the login session.',
1540 { desc => 'Limit (optional)', type => 'number'},
1541 { desc => 'Offset (optional)', type => 'number'},
1544 desc => 'Holds count (integer), or event on failure',
1550 __PACKAGE__->register_method(
1551 method => "hold_pull_list",
1553 # TODO: tag with api_level 2 once fully supported
1554 api_name => "open-ils.circ.hold_pull_list.fleshed.stream",
1556 desc => q/Returns a stream of fleshed holds that need to be
1557 "pulled" by a given location. The location is
1558 determined by the login session.
1559 This API calls always run in authoritative mode./,
1561 { desc => 'Limit (optional)', type => 'number'},
1562 { desc => 'Offset (optional)', type => 'number'},
1565 desc => 'Stream of holds holds, or event on failure',
1570 sub hold_pull_list {
1571 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1572 my( $reqr, $evt ) = $U->checkses($authtoken);
1573 return $evt if $evt;
1575 my $org = $reqr->ws_ou || $reqr->home_ou;
1576 # the perm locaiton shouldn't really matter here since holds
1577 # will exist all over and VIEW_HOLDS should be universal
1578 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1579 return $evt if $evt;
1581 if($self->api_name =~ /count/) {
1583 my $count = $U->storagereq(
1584 'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1585 $org, $limit, $offset );
1587 $logger->info("Grabbing pull list for org unit $org with $count items");
1590 } elsif( $self->api_name =~ /id_list/ ) {
1592 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1593 $org, $limit, $offset );
1595 } elsif ($self->api_name =~ /fleshed/) {
1597 my $ids = $U->storagereq(
1598 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1599 $org, $limit, $offset );
1601 my $e = new_editor(xact => 1, requestor => $reqr);
1602 $conn->respond(uber_hold_impl($e, $_, {flesh_acpl => 1})) for @$ids;
1604 $conn->respond_complete;
1609 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1610 $org, $limit, $offset );
1614 __PACKAGE__->register_method(
1615 method => "print_hold_pull_list",
1616 api_name => "open-ils.circ.hold_pull_list.print",
1618 desc => 'Returns an HTML-formatted holds pull list',
1620 { desc => 'Authtoken', type => 'string'},
1621 { desc => 'Org unit ID. Optional, defaults to workstation org unit', type => 'number'},
1624 desc => 'HTML string',
1630 sub print_hold_pull_list {
1631 my($self, $client, $auth, $org_id) = @_;
1633 my $e = new_editor(authtoken=>$auth);
1634 return $e->event unless $e->checkauth;
1636 $org_id = (defined $org_id) ? $org_id : $e->requestor->ws_ou;
1637 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1639 my $hold_ids = $U->storagereq(
1640 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1643 return undef unless @$hold_ids;
1645 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1647 # Holds will /NOT/ be in order after this ...
1648 my $holds = $e->search_action_hold_request({id => $hold_ids}, {substream => 1});
1649 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1651 # ... so we must resort.
1652 my $hold_map = +{map { $_->id => $_ } @$holds};
1653 my $sorted_holds = [];
1654 push @$sorted_holds, $hold_map->{$_} foreach @$hold_ids;
1656 return $U->fire_object_event(
1657 undef, "ahr.format.pull_list", $sorted_holds,
1658 $org_id, undef, undef, $client
1663 __PACKAGE__->register_method(
1664 method => "print_hold_pull_list_stream",
1666 api_name => "open-ils.circ.hold_pull_list.print.stream",
1668 desc => 'Returns a stream of fleshed holds',
1670 { desc => 'Authtoken', type => 'string'},
1671 { 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)',
1676 desc => 'A stream of fleshed holds',
1682 sub print_hold_pull_list_stream {
1683 my($self, $client, $auth, $params) = @_;
1685 my $e = new_editor(authtoken=>$auth);
1686 return $e->die_event unless $e->checkauth;
1688 delete($$params{org_id}) unless (int($$params{org_id}));
1689 delete($$params{limit}) unless (int($$params{limit}));
1690 delete($$params{offset}) unless (int($$params{offset}));
1691 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
1692 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
1693 $$params{chunk_size} ||= 10;
1694 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
1696 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
1697 return $e->die_event unless $e->allowed('VIEW_HOLD', $$params{org_id });
1700 if ($$params{sort} && @{ $$params{sort} }) {
1701 for my $s (@{ $$params{sort} }) {
1702 if ($s eq 'acplo.position') {
1704 "class" => "acplo", "field" => "position",
1705 "transform" => "coalesce", "params" => [999]
1707 } elsif ($s eq 'prefix') {
1708 push @$sort, {"class" => "acnp", "field" => "label_sortkey"};
1709 } elsif ($s eq 'call_number') {
1710 push @$sort, {"class" => "acn", "field" => "label_sortkey"};
1711 } elsif ($s eq 'suffix') {
1712 push @$sort, {"class" => "acns", "field" => "label_sortkey"};
1713 } elsif ($s eq 'request_time') {
1714 push @$sort, {"class" => "ahr", "field" => "request_time"};
1718 push @$sort, {"class" => "ahr", "field" => "request_time"};
1721 my $holds_ids = $e->json_query(
1723 "select" => {"ahr" => ["id"]},
1728 "fkey" => "current_copy",
1730 "circ_lib" => $$params{org_id}, "status" => [0,7]
1735 "fkey" => "call_number",
1749 "fkey" => "circ_lib",
1752 "location" => {"=" => {"+acp" => "location"}}
1761 "capture_time" => undef,
1762 "cancel_time" => undef,
1764 {"expire_time" => undef },
1765 {"expire_time" => {">" => "now"}}
1769 (@$sort ? (order_by => $sort) : ()),
1770 ($$params{limit} ? (limit => $$params{limit}) : ()),
1771 ($$params{offset} ? (offset => $$params{offset}) : ())
1772 }, {"substream" => 1}
1773 ) or return $e->die_event;
1775 $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
1778 for my $hid (@$holds_ids) {
1779 push @chunk, $e->retrieve_action_hold_request([
1783 "ahr" => ["usr", "current_copy"],
1785 "acp" => ["location", "call_number", "parts"],
1786 "acn" => ["record","prefix","suffix"]
1791 if (@chunk >= $$params{chunk_size}) {
1792 $client->respond( \@chunk );
1796 $client->respond_complete( \@chunk ) if (@chunk);
1803 __PACKAGE__->register_method(
1804 method => 'fetch_hold_notify',
1805 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
1808 Returns a list of hold notification objects based on hold id.
1809 @param authtoken The loggin session key
1810 @param holdid The id of the hold whose notifications we want to retrieve
1811 @return An array of hold notification objects, event on error.
1815 sub fetch_hold_notify {
1816 my( $self, $conn, $authtoken, $holdid ) = @_;
1817 my( $requestor, $evt ) = $U->checkses($authtoken);
1818 return $evt if $evt;
1819 my ($hold, $patron);
1820 ($hold, $evt) = $U->fetch_hold($holdid);
1821 return $evt if $evt;
1822 ($patron, $evt) = $U->fetch_user($hold->usr);
1823 return $evt if $evt;
1825 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
1826 return $evt if $evt;
1828 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
1829 return $U->cstorereq(
1830 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
1834 __PACKAGE__->register_method(
1835 method => 'create_hold_notify',
1836 api_name => 'open-ils.circ.hold_notification.create',
1838 Creates a new hold notification object
1839 @param authtoken The login session key
1840 @param notification The hold notification object to create
1841 @return ID of the new object on success, Event on error
1845 sub create_hold_notify {
1846 my( $self, $conn, $auth, $note ) = @_;
1847 my $e = new_editor(authtoken=>$auth, xact=>1);
1848 return $e->die_event unless $e->checkauth;
1850 my $hold = $e->retrieve_action_hold_request($note->hold)
1851 or return $e->die_event;
1852 my $patron = $e->retrieve_actor_user($hold->usr)
1853 or return $e->die_event;
1855 return $e->die_event unless
1856 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
1858 $note->notify_staff($e->requestor->id);
1859 $e->create_action_hold_notification($note) or return $e->die_event;
1864 __PACKAGE__->register_method(
1865 method => 'create_hold_note',
1866 api_name => 'open-ils.circ.hold_note.create',
1868 Creates a new hold request note object
1869 @param authtoken The login session key
1870 @param note The hold note object to create
1871 @return ID of the new object on success, Event on error
1875 sub create_hold_note {
1876 my( $self, $conn, $auth, $note ) = @_;
1877 my $e = new_editor(authtoken=>$auth, xact=>1);
1878 return $e->die_event unless $e->checkauth;
1880 my $hold = $e->retrieve_action_hold_request($note->hold)
1881 or return $e->die_event;
1882 my $patron = $e->retrieve_actor_user($hold->usr)
1883 or return $e->die_event;
1885 return $e->die_event unless
1886 $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
1888 $e->create_action_hold_request_note($note) or return $e->die_event;
1893 __PACKAGE__->register_method(
1894 method => 'reset_hold',
1895 api_name => 'open-ils.circ.hold.reset',
1897 Un-captures and un-targets a hold, essentially returning
1898 it to the state it was in directly after it was placed,
1899 then attempts to re-target the hold
1900 @param authtoken The login session key
1901 @param holdid The id of the hold
1907 my( $self, $conn, $auth, $holdid ) = @_;
1909 my ($hold, $evt) = $U->fetch_hold($holdid);
1910 return $evt if $evt;
1911 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1912 return $evt if $evt;
1913 $evt = _reset_hold($self, $reqr, $hold);
1914 return $evt if $evt;
1919 __PACKAGE__->register_method(
1920 method => 'reset_hold_batch',
1921 api_name => 'open-ils.circ.hold.reset.batch'
1924 sub reset_hold_batch {
1925 my($self, $conn, $auth, $hold_ids) = @_;
1927 my $e = new_editor(authtoken => $auth);
1928 return $e->event unless $e->checkauth;
1930 for my $hold_id ($hold_ids) {
1932 my $hold = $e->retrieve_action_hold_request(
1933 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
1934 or return $e->event;
1936 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1937 _reset_hold($self, $e->requestor, $hold);
1945 my ($self, $reqr, $hold) = @_;
1947 my $e = new_editor(xact =>1, requestor => $reqr);
1949 $logger->info("reseting hold ".$hold->id);
1951 my $hid = $hold->id;
1953 if( $hold->capture_time and $hold->current_copy ) {
1955 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1956 or return $e->die_event;
1958 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1959 $logger->info("setting copy to status 'reshelving' on hold retarget");
1960 $copy->status(OILS_COPY_STATUS_RESHELVING);
1961 $copy->editor($e->requestor->id);
1962 $copy->edit_date('now');
1963 $e->update_asset_copy($copy) or return $e->die_event;
1965 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1967 # We don't want the copy to remain "in transit"
1968 $copy->status(OILS_COPY_STATUS_RESHELVING);
1969 $logger->warn("! reseting hold [$hid] that is in transit");
1970 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
1973 my $trans = $e->retrieve_action_transit_copy($transid);
1975 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1976 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
1977 $logger->info("Transit abort completed with result $evt");
1978 unless ("$evt" eq 1) {
1987 $hold->clear_capture_time;
1988 $hold->clear_current_copy;
1989 $hold->clear_shelf_time;
1990 $hold->clear_shelf_expire_time;
1991 $hold->clear_current_shelf_lib;
1993 $e->update_action_hold_request($hold) or return $e->die_event;
1997 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
2003 __PACKAGE__->register_method(
2004 method => 'fetch_open_title_holds',
2005 api_name => 'open-ils.circ.open_holds.retrieve',
2007 Returns a list ids of un-fulfilled holds for a given title id
2008 @param authtoken The login session key
2009 @param id the id of the item whose holds we want to retrieve
2010 @param type The hold type - M, T, I, V, C, F, R
2014 sub fetch_open_title_holds {
2015 my( $self, $conn, $auth, $id, $type, $org ) = @_;
2016 my $e = new_editor( authtoken => $auth );
2017 return $e->event unless $e->checkauth;
2020 $org ||= $e->requestor->ws_ou;
2022 # return $e->search_action_hold_request(
2023 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
2025 # XXX make me return IDs in the future ^--
2026 my $holds = $e->search_action_hold_request(
2029 cancel_time => undef,
2031 fulfillment_time => undef
2035 flesh_hold_transits($holds);
2040 sub flesh_hold_transits {
2042 for my $hold ( @$holds ) {
2044 $apputils->simplereq(
2046 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
2047 { hold => $hold->id },
2048 { order_by => { ahtc => 'id desc' }, limit => 1 }
2054 sub flesh_hold_notices {
2055 my( $holds, $e ) = @_;
2056 $e ||= new_editor();
2058 for my $hold (@$holds) {
2059 my $notices = $e->search_action_hold_notification(
2061 { hold => $hold->id },
2062 { order_by => { anh => 'notify_time desc' } },
2067 $hold->notify_count(scalar(@$notices));
2069 my $n = $e->retrieve_action_hold_notification($$notices[0])
2070 or return $e->event;
2071 $hold->notify_time($n->notify_time);
2077 __PACKAGE__->register_method(
2078 method => 'fetch_captured_holds',
2079 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
2083 Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
2084 @param authtoken The login session key
2085 @param org The org id of the location in question
2086 @param match_copy A specific copy to limit to
2090 __PACKAGE__->register_method(
2091 method => 'fetch_captured_holds',
2092 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
2096 Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
2097 @param authtoken The login session key
2098 @param org The org id of the location in question
2099 @param match_copy A specific copy to limit to
2103 __PACKAGE__->register_method(
2104 method => 'fetch_captured_holds',
2105 api_name => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
2109 Returns list ids of shelf-expired un-fulfilled holds for a given title id
2110 @param authtoken The login session key
2111 @param org The org id of the location in question
2112 @param match_copy A specific copy to limit to
2116 __PACKAGE__->register_method(
2117 method => 'fetch_captured_holds',
2119 'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
2123 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2124 for a given shelf lib
2128 __PACKAGE__->register_method(
2129 method => 'fetch_captured_holds',
2131 'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
2135 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2136 for a given shelf lib
2141 sub fetch_captured_holds {
2142 my( $self, $conn, $auth, $org, $match_copy ) = @_;
2144 my $e = new_editor(authtoken => $auth);
2145 return $e->die_event unless $e->checkauth;
2146 return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
2148 $org ||= $e->requestor->ws_ou;
2150 my $current_copy = { '!=' => undef };
2151 $current_copy = { '=' => $match_copy } if $match_copy;
2154 select => { alhr => ['id'] },
2159 fkey => 'current_copy'
2164 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
2166 capture_time => { "!=" => undef },
2167 current_copy => $current_copy,
2168 fulfillment_time => undef,
2169 current_shelf_lib => $org
2173 if($self->api_name =~ /expired/) {
2174 $query->{'where'}->{'+alhr'}->{'-or'} = {
2175 shelf_expire_time => { '<' => 'today'},
2176 cancel_time => { '!=' => undef },
2179 my $hold_ids = $e->json_query( $query );
2181 if ($self->api_name =~ /wrong_shelf/) {
2182 # fetch holds whose current_shelf_lib is $org, but whose pickup
2183 # lib is some other org unit. Ignore already-retrieved holds.
2185 pickup_lib_changed_on_shelf_holds(
2186 $e, $org, [map {$_->{id}} @$hold_ids]);
2187 # match the layout of other items in $hold_ids
2188 push (@$hold_ids, {id => $_}) for @$wrong_shelf;
2192 for my $hold_id (@$hold_ids) {
2193 if($self->api_name =~ /id_list/) {
2194 $conn->respond($hold_id->{id});
2198 $e->retrieve_action_hold_request([
2202 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
2203 order_by => {anh => 'notify_time desc'}
2213 __PACKAGE__->register_method(
2214 method => "print_expired_holds_stream",
2215 api_name => "open-ils.circ.captured_holds.expired.print.stream",
2219 sub print_expired_holds_stream {
2220 my ($self, $client, $auth, $params) = @_;
2222 # No need to check specific permissions: we're going to call another method
2223 # that will do that.
2224 my $e = new_editor("authtoken" => $auth);
2225 return $e->die_event unless $e->checkauth;
2227 delete($$params{org_id}) unless (int($$params{org_id}));
2228 delete($$params{limit}) unless (int($$params{limit}));
2229 delete($$params{offset}) unless (int($$params{offset}));
2230 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2231 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2232 $$params{chunk_size} ||= 10;
2233 $client->max_chunk_size($$params{chunk_size}) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
2235 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2237 my @hold_ids = $self->method_lookup(
2238 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
2239 )->run($auth, $params->{"org_id"});
2244 } elsif (defined $U->event_code($hold_ids[0])) {
2246 return $hold_ids[0];
2249 $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
2252 my @hid_chunk = splice @hold_ids, 0, $params->{"chunk_size"};
2254 my $result_chunk = $e->json_query({
2256 "acp" => ["barcode"],
2258 first_given_name second_given_name family_name alias
2267 "field" => "id", "fkey" => "current_copy",
2270 "field" => "id", "fkey" => "call_number",
2273 "field" => "id", "fkey" => "record"
2277 "acpl" => {"field" => "id", "fkey" => "location"}
2280 "au" => {"field" => "id", "fkey" => "usr"}
2283 "where" => {"+ahr" => {"id" => \@hid_chunk}}
2284 }) or return $e->die_event;
2285 $client->respond($result_chunk);
2292 __PACKAGE__->register_method(
2293 method => "check_title_hold_batch",
2294 api_name => "open-ils.circ.title_hold.is_possible.batch",
2297 desc => '@see open-ils.circ.title_hold.is_possible.batch',
2299 { desc => 'Authentication token', type => 'string'},
2300 { desc => 'Array of Hash of named parameters', type => 'array'},
2303 desc => 'Array of response objects',
2309 sub check_title_hold_batch {
2310 my($self, $client, $authtoken, $param_list, $oargs) = @_;
2311 foreach (@$param_list) {
2312 my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
2313 $client->respond($res);
2319 __PACKAGE__->register_method(
2320 method => "check_title_hold",
2321 api_name => "open-ils.circ.title_hold.is_possible",
2323 desc => 'Determines if a hold were to be placed by a given user, ' .
2324 'whether or not said hold would have any potential copies to fulfill it.' .
2325 'The named paramaters of the second argument include: ' .
2326 'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
2327 'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
2329 { desc => 'Authentication token', type => 'string'},
2330 { desc => 'Hash of named parameters', type => 'object'},
2333 desc => 'List of new message IDs (empty if none)',
2339 =head3 check_title_hold (token, hash)
2341 The named fields in the hash are:
2343 patronid - ID of the hold recipient (required)
2344 depth - hold range depth (default 0)
2345 pickup_lib - destination for hold, fallback value for selection_ou
2346 selection_ou - ID of org_unit establishing hard and soft hold boundary settings
2347 issuanceid - ID of the issuance to be held, required for Issuance level hold
2348 partid - ID of the monograph part to be held, required for monograph part level hold
2349 titleid - ID (BRN) of the title to be held, required for Title level hold
2350 volume_id - required for Volume level hold
2351 copy_id - required for Copy level hold
2352 mrid - required for Meta-record level hold
2353 hold_type - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record (default "T")
2355 All key/value pairs are passed on to do_possibility_checks.
2359 # FIXME: better params checking. what other params are required, if any?
2360 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
2361 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
2362 # used in conditionals, where it may be undefined, causing a warning.
2363 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
2365 sub check_title_hold {
2366 my( $self, $client, $authtoken, $params ) = @_;
2367 my $e = new_editor(authtoken=>$authtoken);
2368 return $e->event unless $e->checkauth;
2370 my %params = %$params;
2371 my $depth = $params{depth} || 0;
2372 my $selection_ou = $params{selection_ou} || $params{pickup_lib};
2373 my $oargs = $params{oargs} || {};
2375 if($oargs->{events}) {
2376 @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
2380 my $patron = $e->retrieve_actor_user($params{patronid})
2381 or return $e->event;
2383 if( $e->requestor->id ne $patron->id ) {
2384 return $e->event unless
2385 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
2388 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
2390 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
2391 or return $e->event;
2393 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
2394 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
2397 my $return_depth = $hard_boundary; # default depth to return on success
2398 if(defined $soft_boundary and $depth < $soft_boundary) {
2399 # work up the tree and as soon as we find a potential copy, use that depth
2400 # also, make sure we don't go past the hard boundary if it exists
2402 # our min boundary is the greater of user-specified boundary or hard boundary
2403 my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
2404 $hard_boundary : $depth;
2406 my $depth = $soft_boundary;
2407 while($depth >= $min_depth) {
2408 $logger->info("performing hold possibility check with soft boundary $depth");
2409 @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
2411 $return_depth = $depth;
2416 } elsif(defined $hard_boundary and $depth < $hard_boundary) {
2417 # there is no soft boundary, enforce the hard boundary if it exists
2418 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
2419 @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
2421 # no boundaries defined, fall back to user specifed boundary or no boundary
2422 $logger->info("performing hold possibility check with no boundary");
2423 @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
2426 my $place_unfillable = 0;
2427 $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
2432 "depth" => $return_depth,
2433 "local_avail" => $status[1]
2435 } elsif ($status[2]) {
2436 my $n = scalar @{$status[2]};
2437 return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2439 return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2445 sub do_possibility_checks {
2446 my($e, $patron, $request_lib, $depth, %params) = @_;
2448 my $issuanceid = $params{issuanceid} || "";
2449 my $partid = $params{partid} || "";
2450 my $titleid = $params{titleid} || "";
2451 my $volid = $params{volume_id};
2452 my $copyid = $params{copy_id};
2453 my $mrid = $params{mrid} || "";
2454 my $pickup_lib = $params{pickup_lib};
2455 my $hold_type = $params{hold_type} || 'T';
2456 my $selection_ou = $params{selection_ou} || $pickup_lib;
2457 my $holdable_formats = $params{holdable_formats};
2458 my $oargs = $params{oargs} || {};
2465 if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
2467 return $e->event unless $copy = $e->retrieve_asset_copy($copyid);
2468 return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
2469 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2471 return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
2472 return verify_copy_for_hold(
2473 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
2476 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2478 return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
2479 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2481 return _check_volume_hold_is_possible(
2482 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2485 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
2487 return _check_title_hold_is_possible(
2488 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
2491 } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
2493 return _check_issuance_hold_is_possible(
2494 $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2497 } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
2499 return _check_monopart_hold_is_possible(
2500 $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2503 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2505 # pasing undef as the depth to filtered_records causes the depth
2506 # of the selection_ou to be used, which is not what we want here.
2509 my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
2511 for my $rec (@$recs) {
2512 @status = _check_title_hold_is_possible(
2513 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
2519 # else { Unrecognized hold_type ! } # FIXME: return error? or 0?
2522 sub MR_filter_records {
2529 my $opac_visible = shift;
2531 my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
2532 return $U->storagereq(
2533 'open-ils.storage.metarecord.filtered_records.atomic',
2534 $m, $f, $org_at_depth, $opac_visible
2537 __PACKAGE__->register_method(
2538 method => 'MR_filter_records',
2539 api_name => 'open-ils.circ.holds.metarecord.filtered_records',
2544 sub create_ranged_org_filter {
2545 my($e, $selection_ou, $depth) = @_;
2547 # find the orgs from which this hold may be fulfilled,
2548 # based on the selection_ou and depth
2550 my $top_org = $e->search_actor_org_unit([
2551 {parent_ou => undef},
2552 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
2555 return () if $depth == $top_org->ou_type->depth;
2557 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
2558 %org_filter = (circ_lib => []);
2559 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
2561 $logger->info("hold org filter at depth $depth and selection_ou ".
2562 "$selection_ou created list of @{$org_filter{circ_lib}}");
2568 sub _check_title_hold_is_possible {
2569 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
2570 # $holdable_formats is now unused. We pre-filter the MR's records.
2572 my $e = new_editor();
2573 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2575 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2576 my $copies = $e->json_query(
2578 select => { acp => ['id', 'circ_lib'] },
2583 fkey => 'call_number',
2584 filter => { record => $titleid }
2588 filter => { holdable => 't', deleted => 'f' },
2591 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' },
2592 acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2596 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2597 '+acpm' => { target_copy => undef } # ignore part-linked copies
2602 $logger->info("title possible found ".scalar(@$copies)." potential copies");
2606 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2607 "payload" => {"fail_part" => "no_ultimate_items"}
2612 # -----------------------------------------------------------------------
2613 # sort the copies into buckets based on their circ_lib proximity to
2614 # the patron's home_ou.
2615 # -----------------------------------------------------------------------
2617 my $home_org = $patron->home_ou;
2618 my $req_org = $request_lib->id;
2620 $prox_cache{$home_org} =
2621 $e->search_actor_org_unit_proximity({from_org => $home_org})
2622 unless $prox_cache{$home_org};
2623 my $home_prox = $prox_cache{$home_org};
2624 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2627 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2628 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2630 my @keys = sort { $a <=> $b } keys %buckets;
2633 if( $home_org ne $req_org ) {
2634 # -----------------------------------------------------------------------
2635 # shove the copies close to the request_lib into the primary buckets
2636 # directly before the farthest away copies. That way, they are not
2637 # given priority, but they are checked before the farthest copies.
2638 # -----------------------------------------------------------------------
2639 $prox_cache{$req_org} =
2640 $e->search_actor_org_unit_proximity({from_org => $req_org})
2641 unless $prox_cache{$req_org};
2642 my $req_prox = $prox_cache{$req_org};
2645 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2646 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2648 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2649 my $new_key = $highest_key - 0.5; # right before the farthest prox
2650 my @keys2 = sort { $a <=> $b } keys %buckets2;
2651 for my $key (@keys2) {
2652 last if $key >= $highest_key;
2653 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2657 @keys = sort { $a <=> $b } keys %buckets;
2662 my $age_protect_only = 0;
2663 OUTER: for my $key (@keys) {
2664 my @cps = @{$buckets{$key}};
2666 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2668 for my $copyid (@cps) {
2670 next if $seen{$copyid};
2671 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2672 my $copy = $e->retrieve_asset_copy($copyid);
2673 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2675 unless($title) { # grab the title if we don't already have it
2676 my $vol = $e->retrieve_asset_call_number(
2677 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2678 $title = $vol->record;
2681 @status = verify_copy_for_hold(
2682 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2684 $age_protect_only ||= $status[3];
2685 last OUTER if $status[0];
2689 $status[3] = $age_protect_only;
2693 sub _check_issuance_hold_is_possible {
2694 my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2696 my $e = new_editor();
2697 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2699 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2700 my $copies = $e->json_query(
2702 select => { acp => ['id', 'circ_lib'] },
2708 filter => { issuance => $issuanceid }
2712 filter => { holdable => 't', deleted => 'f' },
2715 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
2719 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
2725 $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
2729 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
2730 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2735 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2736 "payload" => {"fail_part" => "no_ultimate_items"}
2744 # -----------------------------------------------------------------------
2745 # sort the copies into buckets based on their circ_lib proximity to
2746 # the patron's home_ou.
2747 # -----------------------------------------------------------------------
2749 my $home_org = $patron->home_ou;
2750 my $req_org = $request_lib->id;
2752 $prox_cache{$home_org} =
2753 $e->search_actor_org_unit_proximity({from_org => $home_org})
2754 unless $prox_cache{$home_org};
2755 my $home_prox = $prox_cache{$home_org};
2756 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2759 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2760 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2762 my @keys = sort { $a <=> $b } keys %buckets;
2765 if( $home_org ne $req_org ) {
2766 # -----------------------------------------------------------------------
2767 # shove the copies close to the request_lib into the primary buckets
2768 # directly before the farthest away copies. That way, they are not
2769 # given priority, but they are checked before the farthest copies.
2770 # -----------------------------------------------------------------------
2771 $prox_cache{$req_org} =
2772 $e->search_actor_org_unit_proximity({from_org => $req_org})
2773 unless $prox_cache{$req_org};
2774 my $req_prox = $prox_cache{$req_org};
2777 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2778 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2780 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2781 my $new_key = $highest_key - 0.5; # right before the farthest prox
2782 my @keys2 = sort { $a <=> $b } keys %buckets2;
2783 for my $key (@keys2) {
2784 last if $key >= $highest_key;
2785 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2789 @keys = sort { $a <=> $b } keys %buckets;
2794 my $age_protect_only = 0;
2795 OUTER: for my $key (@keys) {
2796 my @cps = @{$buckets{$key}};
2798 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2800 for my $copyid (@cps) {
2802 next if $seen{$copyid};
2803 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2804 my $copy = $e->retrieve_asset_copy($copyid);
2805 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2807 unless($title) { # grab the title if we don't already have it
2808 my $vol = $e->retrieve_asset_call_number(
2809 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2810 $title = $vol->record;
2813 @status = verify_copy_for_hold(
2814 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2816 $age_protect_only ||= $status[3];
2817 last OUTER if $status[0];
2822 if (!defined($empty_ok)) {
2823 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
2824 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2827 return (1,0) if ($empty_ok);
2829 $status[3] = $age_protect_only;
2833 sub _check_monopart_hold_is_possible {
2834 my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2836 my $e = new_editor();
2837 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2839 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2840 my $copies = $e->json_query(
2842 select => { acp => ['id', 'circ_lib'] },
2846 field => 'target_copy',
2848 filter => { part => $partid }
2852 filter => { holdable => 't', deleted => 'f' },
2855 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
2859 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
2865 $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
2869 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
2870 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2875 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2876 "payload" => {"fail_part" => "no_ultimate_items"}
2884 # -----------------------------------------------------------------------
2885 # sort the copies into buckets based on their circ_lib proximity to
2886 # the patron's home_ou.
2887 # -----------------------------------------------------------------------
2889 my $home_org = $patron->home_ou;
2890 my $req_org = $request_lib->id;
2892 $prox_cache{$home_org} =
2893 $e->search_actor_org_unit_proximity({from_org => $home_org})
2894 unless $prox_cache{$home_org};
2895 my $home_prox = $prox_cache{$home_org};
2896 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2899 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2900 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2902 my @keys = sort { $a <=> $b } keys %buckets;
2905 if( $home_org ne $req_org ) {
2906 # -----------------------------------------------------------------------
2907 # shove the copies close to the request_lib into the primary buckets
2908 # directly before the farthest away copies. That way, they are not
2909 # given priority, but they are checked before the farthest copies.
2910 # -----------------------------------------------------------------------
2911 $prox_cache{$req_org} =
2912 $e->search_actor_org_unit_proximity({from_org => $req_org})
2913 unless $prox_cache{$req_org};
2914 my $req_prox = $prox_cache{$req_org};
2917 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2918 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2920 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2921 my $new_key = $highest_key - 0.5; # right before the farthest prox
2922 my @keys2 = sort { $a <=> $b } keys %buckets2;
2923 for my $key (@keys2) {
2924 last if $key >= $highest_key;
2925 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2929 @keys = sort { $a <=> $b } keys %buckets;
2934 my $age_protect_only = 0;
2935 OUTER: for my $key (@keys) {
2936 my @cps = @{$buckets{$key}};
2938 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2940 for my $copyid (@cps) {
2942 next if $seen{$copyid};
2943 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2944 my $copy = $e->retrieve_asset_copy($copyid);
2945 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2947 unless($title) { # grab the title if we don't already have it
2948 my $vol = $e->retrieve_asset_call_number(
2949 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2950 $title = $vol->record;
2953 @status = verify_copy_for_hold(
2954 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2956 $age_protect_only ||= $status[3];
2957 last OUTER if $status[0];
2962 if (!defined($empty_ok)) {
2963 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
2964 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2967 return (1,0) if ($empty_ok);
2969 $status[3] = $age_protect_only;
2974 sub _check_volume_hold_is_possible {
2975 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2976 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
2977 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
2978 $logger->info("checking possibility of volume hold for volume ".$vol->id);
2980 my $filter_copies = [];
2981 for my $copy (@$copies) {
2982 # ignore part-mapped copies for regular volume level holds
2983 push(@$filter_copies, $copy) unless
2984 new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
2986 $copies = $filter_copies;
2991 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2992 "payload" => {"fail_part" => "no_ultimate_items"}
2998 my $age_protect_only = 0;
2999 for my $copy ( @$copies ) {
3000 @status = verify_copy_for_hold(
3001 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
3002 $age_protect_only ||= $status[3];
3005 $status[3] = $age_protect_only;
3011 sub verify_copy_for_hold {
3012 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
3013 # $oargs should be undef unless we're overriding.
3014 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
3015 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3018 requestor => $requestor,
3021 title_descriptor => $title->fixed_fields,
3022 pickup_lib => $pickup_lib,
3023 request_lib => $request_lib,
3025 show_event_list => 1
3029 # Check for override permissions on events.
3030 if ($oargs && $permitted && scalar @$permitted) {
3031 # Remove the events from permitted that we can override.
3032 if ($oargs->{events}) {
3033 foreach my $evt (@{$oargs->{events}}) {
3034 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3037 # Now, we handle the override all case by checking remaining
3038 # events against override permissions.
3039 if (scalar @$permitted && $oargs->{all}) {
3040 # Pre-set events and failed members of oargs to empty
3041 # arrays, if they are not set, yet.
3042 $oargs->{events} = [] unless ($oargs->{events});
3043 $oargs->{failed} = [] unless ($oargs->{failed});
3044 # When we're done with these checks, we swap permitted
3045 # with a reference to @disallowed.
3046 my @disallowed = ();
3047 foreach my $evt (@{$permitted}) {
3048 # Check if we've already seen the event in this
3049 # session and it failed.
3050 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3051 push(@disallowed, $evt);
3053 # We have to check if the requestor has the
3054 # override permission.
3056 # AppUtils::check_user_perms returns the perm if
3057 # the user doesn't have it, undef if they do.
3058 if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3059 push(@disallowed, $evt);
3060 push(@{$oargs->{failed}}, $evt->{textcode});
3062 push(@{$oargs->{events}}, $evt->{textcode});
3066 $permitted = \@disallowed;
3070 my $age_protect_only = 0;
3071 if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3072 $age_protect_only = 1;
3076 (not scalar @$permitted), # true if permitted is an empty arrayref
3077 ( # XXX This test is of very dubious value; someone should figure
3078 # out what if anything is checking this value
3079 ($copy->circ_lib == $pickup_lib) and
3080 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3089 sub find_nearest_permitted_hold {
3092 my $editor = shift; # CStoreEditor object
3093 my $copy = shift; # copy to target
3094 my $user = shift; # staff
3095 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3097 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3099 my $bc = $copy->barcode;
3101 # find any existing holds that already target this copy
3102 my $old_holds = $editor->search_action_hold_request(
3103 { current_copy => $copy->id,
3104 cancel_time => undef,
3105 capture_time => undef
3109 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3111 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3112 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3114 my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3116 # the nearest_hold API call now needs this
3117 $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3118 unless ref $copy->call_number;
3120 # search for what should be the best holds for this copy to fulfill
3121 my $best_holds = $U->storagereq(
3122 "open-ils.storage.action.hold_request.nearest_hold.atomic",
3123 $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3125 # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3127 for my $holdid (@$old_holds) {
3128 next unless $holdid;
3129 push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3133 unless(@$best_holds) {
3134 $logger->info("circulator: no suitable holds found for copy $bc");
3135 return (undef, $evt);
3141 # for each potential hold, we have to run the permit script
3142 # to make sure the hold is actually permitted.
3145 for my $holdid (@$best_holds) {
3146 next unless $holdid;
3147 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3149 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3150 # Force and recall holds bypass all rules
3151 if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3155 my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3156 my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3158 $reqr_cache{$hold->requestor} = $reqr;
3159 $org_cache{$hold->request_lib} = $rlib;
3161 # see if this hold is permitted
3162 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3164 patron_id => $hold->usr,
3167 pickup_lib => $hold->pickup_lib,
3168 request_lib => $rlib,
3180 unless( $best_hold ) { # no "good" permitted holds were found
3182 $logger->info("circulator: no suitable holds found for copy $bc");
3183 return (undef, $evt);
3186 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3188 # indicate a permitted hold was found
3189 return $best_hold if $check_only;
3191 # we've found a permitted hold. we need to "grab" the copy
3192 # to prevent re-targeted holds (next part) from re-grabbing the copy
3193 $best_hold->current_copy($copy->id);
3194 $editor->update_action_hold_request($best_hold)
3195 or return (undef, $editor->event);
3200 # re-target any other holds that already target this copy
3201 for my $old_hold (@$old_holds) {
3202 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3203 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3204 $old_hold->id." after a better hold [".$best_hold->id."] was found");
3205 $old_hold->clear_current_copy;
3206 $old_hold->clear_prev_check_time;
3207 $editor->update_action_hold_request($old_hold)
3208 or return (undef, $editor->event);
3209 push(@retarget, $old_hold->id);
3212 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3220 __PACKAGE__->register_method(
3221 method => 'all_rec_holds',
3222 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3226 my( $self, $conn, $auth, $title_id, $args ) = @_;
3228 my $e = new_editor(authtoken=>$auth);
3229 $e->checkauth or return $e->event;
3230 $e->allowed('VIEW_HOLD') or return $e->event;
3233 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
3234 $args->{cancel_time} = undef;
3237 metarecord_holds => []
3239 , volume_holds => []
3241 , recall_holds => []
3244 , issuance_holds => []
3247 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3249 $resp->{metarecord_holds} = $e->search_action_hold_request(
3250 { hold_type => OILS_HOLD_TYPE_METARECORD,
3251 target => $mr_map->metarecord,
3257 $resp->{title_holds} = $e->search_action_hold_request(
3259 hold_type => OILS_HOLD_TYPE_TITLE,
3260 target => $title_id,
3264 my $parts = $e->search_biblio_monograph_part(
3270 $resp->{part_holds} = $e->search_action_hold_request(
3272 hold_type => OILS_HOLD_TYPE_MONOPART,
3278 my $subs = $e->search_serial_subscription(
3279 { record_entry => $title_id }, {idlist=>1});
3282 my $issuances = $e->search_serial_issuance(
3283 {subscription => $subs}, {idlist=>1}
3287 $resp->{issuance_holds} = $e->search_action_hold_request(
3289 hold_type => OILS_HOLD_TYPE_ISSUANCE,
3290 target => $issuances,
3297 my $vols = $e->search_asset_call_number(
3298 { record => $title_id, deleted => 'f' }, {idlist=>1});
3300 return $resp unless @$vols;
3302 $resp->{volume_holds} = $e->search_action_hold_request(
3304 hold_type => OILS_HOLD_TYPE_VOLUME,
3309 my $copies = $e->search_asset_copy(
3310 { call_number => $vols, deleted => 'f' }, {idlist=>1});
3312 return $resp unless @$copies;
3314 $resp->{copy_holds} = $e->search_action_hold_request(
3316 hold_type => OILS_HOLD_TYPE_COPY,
3321 $resp->{recall_holds} = $e->search_action_hold_request(
3323 hold_type => OILS_HOLD_TYPE_RECALL,
3328 $resp->{force_holds} = $e->search_action_hold_request(
3330 hold_type => OILS_HOLD_TYPE_FORCE,
3342 __PACKAGE__->register_method(
3343 method => 'uber_hold',
3345 api_name => 'open-ils.circ.hold.details.retrieve'
3349 my($self, $client, $auth, $hold_id, $args) = @_;
3350 my $e = new_editor(authtoken=>$auth);
3351 $e->checkauth or return $e->event;
3352 return uber_hold_impl($e, $hold_id, $args);
3355 __PACKAGE__->register_method(
3356 method => 'batch_uber_hold',
3359 api_name => 'open-ils.circ.hold.details.batch.retrieve'
3362 sub batch_uber_hold {
3363 my($self, $client, $auth, $hold_ids, $args) = @_;
3364 my $e = new_editor(authtoken=>$auth);
3365 $e->checkauth or return $e->event;
3366 $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3370 sub uber_hold_impl {
3371 my($e, $hold_id, $args) = @_;
3374 my $hold = $e->retrieve_action_hold_request(
3379 flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
3382 ) or return $e->event;
3384 if($hold->usr->id ne $e->requestor->id) {
3385 # caller is asking for someone else's hold
3386 $e->allowed('VIEW_HOLD') or return $e->event;
3387 $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3388 [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3391 # caller is asking for own hold, but may not have permission to view staff notes
3392 unless($e->allowed('VIEW_HOLD')) {
3393 $hold->notes( # filter out any staff notes (unless marked as public)
3394 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3398 my $user = $hold->usr;
3399 $hold->usr($user->id);
3402 my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args->{suppress_mvr});
3404 flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3405 flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3407 my $details = retrieve_hold_queue_status_impl($e, $hold);
3412 ($copy ? (copy => $copy) : ()),
3413 ($volume ? (volume => $volume) : ()),
3414 ($issuance ? (issuance => $issuance) : ()),
3415 ($part ? (part => $part) : ()),
3416 ($args->{include_bre} ? (bre => $bre) : ()),
3417 ($args->{suppress_mvr} ? () : (mvr => $mvr)),
3421 $resp->{copy}->location(
3422 $e->retrieve_asset_copy_location($resp->{copy}->location))
3423 if $resp->{copy} and $args->{flesh_acpl};
3425 unless($args->{suppress_patron_details}) {
3426 my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3427 $resp->{patron_first} = $user->first_given_name,
3428 $resp->{patron_last} = $user->family_name,
3429 $resp->{patron_barcode} = $card->barcode,
3430 $resp->{patron_alias} = $user->alias,
3438 # -----------------------------------------------------
3439 # Returns the MVR object that represents what the
3441 # -----------------------------------------------------
3443 my( $e, $hold, $no_mvr ) = @_;
3451 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3452 my $mr = $e->retrieve_metabib_metarecord($hold->target)
3453 or return $e->event;
3454 $tid = $mr->master_record;
3456 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3457 $tid = $hold->target;
3459 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3460 $volume = $e->retrieve_asset_call_number($hold->target)
3461 or return $e->event;
3462 $tid = $volume->record;
3464 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3465 $issuance = $e->retrieve_serial_issuance([
3467 {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3468 ]) or return $e->event;
3470 $tid = $issuance->subscription->record_entry;
3472 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3473 $part = $e->retrieve_biblio_monograph_part([
3475 ]) or return $e->event;
3477 $tid = $part->record;
3479 } 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 ) {
3480 $copy = $e->retrieve_asset_copy([
3482 {flesh => 1, flesh_fields => {acp => ['call_number']}}
3483 ]) or return $e->event;
3485 $volume = $copy->call_number;
3486 $tid = $volume->record;
3489 if(!$copy and ref $hold->current_copy ) {
3490 $copy = $hold->current_copy;
3491 $hold->current_copy($copy->id);
3494 if(!$volume and $copy) {
3495 $volume = $e->retrieve_asset_call_number($copy->call_number);
3498 # TODO return metarcord mvr for M holds
3499 my $title = $e->retrieve_biblio_record_entry($tid);
3500 return ( ($no_mvr) ? undef : $U->record_to_mvr($title), $volume, $copy, $issuance, $part, $title );
3503 __PACKAGE__->register_method(
3504 method => 'clear_shelf_cache',
3505 api_name => 'open-ils.circ.hold.clear_shelf.get_cache',
3509 Returns the holds processed with the given cache key
3514 sub clear_shelf_cache {
3515 my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3516 my $e = new_editor(authtoken => $auth, xact => 1);
3517 return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3520 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3522 my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3525 $logger->info("no hold data found in cache"); # XXX TODO return event
3531 foreach (keys %$hold_data) {
3532 $maximum += scalar(@{ $hold_data->{$_} });
3534 $client->respond({"maximum" => $maximum, "progress" => 0});
3536 for my $action (sort keys %$hold_data) {
3537 while (@{$hold_data->{$action}}) {
3538 my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3540 my $result_chunk = $e->json_query({
3542 "acp" => ["barcode"],
3544 first_given_name second_given_name family_name alias
3547 "acnp" => [{column => "label", alias => "prefix"}],
3548 "acns" => [{column => "label", alias => "suffix"}],
3556 "field" => "id", "fkey" => "current_copy",
3559 "field" => "id", "fkey" => "call_number",
3562 "field" => "id", "fkey" => "record"
3565 "field" => "id", "fkey" => "prefix"
3568 "field" => "id", "fkey" => "suffix"
3572 "acpl" => {"field" => "id", "fkey" => "location"}
3575 "au" => {"field" => "id", "fkey" => "usr"}
3578 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3579 }, {"substream" => 1}) or return $e->die_event;
3583 +{"action" => $action, "hold_details" => $_}
3594 __PACKAGE__->register_method(
3595 method => 'clear_shelf_process',
3597 api_name => 'open-ils.circ.hold.clear_shelf.process',
3600 1. Find all holds that have expired on the holds shelf
3602 3. If a clear-shelf status is configured, put targeted copies into this status
3603 4. Divide copies into 3 groups: items to transit, items to reshelve, and items
3604 that are needed for holds. No subsequent action is taken on the holds
3605 or items after grouping.
3610 sub clear_shelf_process {
3611 my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
3613 my $e = new_editor(authtoken=>$auth);
3614 $e->checkauth or return $e->die_event;
3615 my $cache = OpenSRF::Utils::Cache->new('global');
3617 $org_id ||= $e->requestor->ws_ou;
3618 $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
3620 my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
3622 my @hold_ids = $self->method_lookup(
3623 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
3624 )->run($auth, $org_id, $match_copy);
3629 my @canceled_holds; # newly canceled holds
3630 $chunk_size ||= 25; # chunked status updates
3631 $client->max_chunk_size($chunk_size) if (!$client->can('max_bundle_size') && $client->can('max_chunk_size'));
3634 for my $hold_id (@hold_ids) {
3636 $logger->info("Clear shelf processing hold $hold_id");
3638 my $hold = $e->retrieve_action_hold_request([
3641 flesh_fields => {ahr => ['current_copy']}
3645 if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
3646 $hold->cancel_time('now');
3647 $hold->cancel_cause(2); # Hold Shelf expiration
3648 $e->update_action_hold_request($hold) or return $e->die_event;
3649 push(@canceled_holds, $hold_id);
3652 my $copy = $hold->current_copy;
3654 if($copy_status or $copy_status == 0) {
3655 # if a clear-shelf copy status is defined, update the copy
3656 $copy->status($copy_status);
3657 $copy->edit_date('now');
3658 $copy->editor($e->requestor->id);
3659 $e->update_asset_copy($copy) or return $e->die_event;
3662 push(@holds, $hold);
3663 $client->respond({maximum => scalar(@holds), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
3672 pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
3675 for my $hold (@holds) {
3677 my $copy = $hold->current_copy;
3678 my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
3680 if($alt_hold and !$match_copy) {
3682 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
3684 } elsif($copy->circ_lib != $e->requestor->ws_ou) {
3686 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
3690 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
3694 my $cache_key = md5_hex(time . $$ . rand());
3695 $logger->info("clear_shelf_cache: storing under $cache_key");
3696 $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours. configurable?
3698 # tell the client we're done
3699 $client->respond_complete({cache_key => $cache_key});
3702 # fire off the hold cancelation trigger and wait for response so don't flood the service
3704 # refetch the holds to pick up the caclulated cancel_time,
3705 # which may be needed by Action/Trigger
3707 my $updated_holds = [];
3708 $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
3711 $U->create_events_for_hook(
3712 'hold_request.cancel.expire_holds_shelf',
3713 $_, $org_id, undef, undef, 1) for @$updated_holds;
3716 # tell the client we're done
3717 $client->respond_complete;
3721 # returns IDs for holds that are on the holds shelf but
3722 # have had their pickup_libs change while on the shelf.
3723 sub pickup_lib_changed_on_shelf_holds {
3726 my $ignore_holds = shift;
3727 $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
3730 select => { alhr => ['id'] },
3735 fkey => 'current_copy'
3740 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
3742 capture_time => { "!=" => undef },
3743 fulfillment_time => undef,
3744 current_shelf_lib => $org_id,
3745 pickup_lib => {'!=' => {'+alhr' => 'current_shelf_lib'}}
3750 $query->{where}->{'+alhr'}->{id} =
3751 {'not in' => $ignore_holds} if @$ignore_holds;
3753 my $hold_ids = $e->json_query($query);
3754 return [ map { $_->{id} } @$hold_ids ];
3757 __PACKAGE__->register_method(
3758 method => 'usr_hold_summary',
3759 api_name => 'open-ils.circ.holds.user_summary',
3761 Returns a summary of holds statuses for a given user
3765 sub usr_hold_summary {
3766 my($self, $conn, $auth, $user_id) = @_;
3768 my $e = new_editor(authtoken=>$auth);
3769 $e->checkauth or return $e->event;
3770 $e->allowed('VIEW_HOLD') or return $e->event;
3772 my $holds = $e->search_action_hold_request(
3775 fulfillment_time => undef,
3776 cancel_time => undef,
3780 my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
3781 $summary{_hold_status($e, $_)} += 1 for @$holds;
3787 __PACKAGE__->register_method(
3788 method => 'hold_has_copy_at',
3789 api_name => 'open-ils.circ.hold.has_copy_at',
3792 'Returns the ID of the found copy and name of the shelving location if there is ' .
3793 'an available copy at the specified org unit. Returns empty hash otherwise. ' .
3794 'The anticipated use for this method is to determine whether an item is ' .
3795 'available at the library where the user is placing the hold (or, alternatively, '.
3796 'at the pickup library) to encourage bypassing the hold placement and just ' .
3797 'checking out the item.' ,
3799 { desc => 'Authentication Token', type => 'string' },
3800 { desc => 'Method Arguments. Options include: hold_type, hold_target, org_unit. '
3801 . 'hold_type is the hold type code (T, V, C, M, ...). '
3802 . 'hold_target is the identifier of the hold target object. '
3803 . 'org_unit is org unit ID.',
3808 desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
3814 sub hold_has_copy_at {
3815 my($self, $conn, $auth, $args) = @_;
3817 my $e = new_editor(authtoken=>$auth);
3818 $e->checkauth or return $e->event;
3820 my $hold_type = $$args{hold_type};
3821 my $hold_target = $$args{hold_target};
3822 my $org_unit = $$args{org_unit};
3825 select => {acp => ['id'], acpl => ['name']},
3830 filter => { holdable => 't', deleted => 'f' },
3833 ccs => {field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3836 where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit, status => [0,7]}},
3840 if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
3842 $query->{where}->{'+acp'}->{id} = $hold_target;
3844 } elsif($hold_type eq 'V') {
3846 $query->{where}->{'+acp'}->{call_number} = $hold_target;
3848 } elsif($hold_type eq 'P') {
3850 $query->{from}->{acp}->{acpm} = {
3851 field => 'target_copy',
3853 filter => {part => $hold_target},
3856 } elsif($hold_type eq 'I') {
3858 $query->{from}->{acp}->{sitem} = {
3861 filter => {issuance => $hold_target},
3864 } elsif($hold_type eq 'T') {
3866 $query->{from}->{acp}->{acn} = {
3868 fkey => 'call_number',
3872 filter => {id => $hold_target},
3880 $query->{from}->{acp}->{acn} = {
3882 fkey => 'call_number',
3891 filter => {metarecord => $hold_target},
3899 my $res = $e->json_query($query)->[0] or return {};
3900 return {copy => $res->{id}, location => $res->{name}} if $res;
3904 # returns true if the user already has an item checked out
3905 # that could be used to fulfill the requested hold.
3906 sub hold_item_is_checked_out {
3907 my($e, $user_id, $hold_type, $hold_target) = @_;
3910 select => {acp => ['id']},
3911 from => {acp => {}},
3915 in => { # copies for circs the user has checked out
3916 select => {circ => ['target_copy']},
3920 checkin_time => undef,
3922 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
3923 {stop_fines => undef}
3933 if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
3935 $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
3937 } elsif($hold_type eq 'V') {
3939 $query->{where}->{'+acp'}->{call_number} = $hold_target;
3941 } elsif($hold_type eq 'P') {
3943 $query->{from}->{acp}->{acpm} = {
3944 field => 'target_copy',
3946 filter => {part => $hold_target},
3949 } elsif($hold_type eq 'I') {
3951 $query->{from}->{acp}->{sitem} = {
3954 filter => {issuance => $hold_target},
3957 } elsif($hold_type eq 'T') {
3959 $query->{from}->{acp}->{acn} = {
3961 fkey => 'call_number',
3965 filter => {id => $hold_target},
3973 $query->{from}->{acp}->{acn} = {
3975 fkey => 'call_number',
3984 filter => {metarecord => $hold_target},
3992 return $e->json_query($query)->[0];
3995 __PACKAGE__->register_method(
3996 method => 'change_hold_title',
3997 api_name => 'open-ils.circ.hold.change_title',
4000 Updates all title level holds targeting the specified bibs to point a new bib./,
4002 { desc => 'Authentication Token', type => 'string' },
4003 { desc => 'New Target Bib Id', type => 'number' },
4004 { desc => 'Old Target Bib Ids', type => 'array' },
4006 return => { desc => '1 on success' }
4010 __PACKAGE__->register_method(
4011 method => 'change_hold_title_for_specific_holds',
4012 api_name => 'open-ils.circ.hold.change_title.specific_holds',
4015 Updates specified holds to target new bib./,
4017 { desc => 'Authentication Token', type => 'string' },
4018 { desc => 'New Target Bib Id', type => 'number' },
4019 { desc => 'Holds Ids for holds to update', type => 'array' },
4021 return => { desc => '1 on success' }
4026 sub change_hold_title {
4027 my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4029 my $e = new_editor(authtoken=>$auth, xact=>1);
4030 return $e->die_event unless $e->checkauth;
4032 my $holds = $e->search_action_hold_request(
4035 cancel_time => undef,
4036 fulfillment_time => undef,
4042 flesh_fields => { ahr => ['usr'] }
4048 for my $hold (@$holds) {
4049 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4050 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4051 $hold->target( $new_bib_id );
4052 $e->update_action_hold_request($hold) or return $e->die_event;
4057 _reset_hold($self, $e->requestor, $_) for @$holds;
4062 sub change_hold_title_for_specific_holds {
4063 my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4065 my $e = new_editor(authtoken=>$auth, xact=>1);
4066 return $e->die_event unless $e->checkauth;
4068 my $holds = $e->search_action_hold_request(
4071 cancel_time => undef,
4072 fulfillment_time => undef,
4078 flesh_fields => { ahr => ['usr'] }
4084 for my $hold (@$holds) {
4085 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4086 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4087 $hold->target( $new_bib_id );
4088 $e->update_action_hold_request($hold) or return $e->die_event;
4093 _reset_hold($self, $e->requestor, $_) for @$holds;
4098 __PACKAGE__->register_method(
4099 method => 'rec_hold_count',
4100 api_name => 'open-ils.circ.bre.holds.count',
4102 desc => q/Returns the total number of holds that target the
4103 selected bib record or its associated copies and call_numbers/,
4105 { desc => 'Bib ID', type => 'number' },
4106 { desc => q/Optional arguments. Supported arguments include:
4107 "pickup_lib_descendant" -> limit holds to those whose pickup
4108 library is equal to or is a child of the provided org unit/,
4112 return => {desc => 'Hold count', type => 'number'}
4116 __PACKAGE__->register_method(
4117 method => 'rec_hold_count',
4118 api_name => 'open-ils.circ.mmr.holds.count',
4120 desc => q/Returns the total number of holds that target the
4121 selected metarecord or its associated copies, call_numbers, and bib records/,
4123 { desc => 'Metarecord ID', type => 'number' },
4125 return => {desc => 'Hold count', type => 'number'}
4129 # XXX Need to add type I holds to these counts
4130 sub rec_hold_count {
4131 my($self, $conn, $target_id, $args) = @_;
4138 filter => {metarecord => $target_id}
4145 filter => { id => $target_id },
4150 if($self->api_name =~ /mmr/) {
4151 delete $bre_join->{bre}->{filter};
4152 $bre_join->{bre}->{join} = $mmr_join;
4158 fkey => 'call_number',
4164 select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4168 cancel_time => undef,
4169 fulfillment_time => undef,
4173 hold_type => [qw/C F R/],
4176 select => {acp => ['id']},
4177 from => { acp => $cn_join }
4187 select => {acn => ['id']},
4188 from => {acn => $bre_join}
4198 select => {bmp => ['id']},
4199 from => {bmp => $bre_join}
4207 target => $target_id
4215 if($self->api_name =~ /mmr/) {
4216 $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4221 select => {bre => ['id']},
4222 from => {bre => $mmr_join}
4228 $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4231 target => $target_id
4237 if (my $pld = $args->{pickup_lib_descendant}) {
4239 my $top_ou = new_editor()->search_actor_org_unit(
4240 {parent_ou => undef}
4241 )->[0]; # XXX Assumes single root node. Not alone in this...
4243 $query->{where}->{'+ahr'}->{pickup_lib} = {
4245 select => {aou => [{
4247 transform => 'actor.org_unit_descendants',
4248 result_field => 'id'
4251 where => {id => $pld}
4253 } if ($pld != $top_ou->id);
4257 return new_editor()->json_query($query)->[0]->{count};
4260 # A helper function to calculate a hold's expiration time at a given
4261 # org_unit. Takes the org_unit as an argument and returns either the
4262 # hold expire time as an ISO8601 string or undef if there is no hold
4263 # expiration interval set for the subject ou.
4264 sub calculate_expire_time
4267 my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4269 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
4270 return $U->epoch2ISO8601($date->epoch);
4276 __PACKAGE__->register_method(
4277 method => 'mr_hold_filter_attrs',
4278 api_name => 'open-ils.circ.mmr.holds.filters',
4283 Returns the set of available formats and languages for the
4284 constituent records of the provided metarcord.
4285 If an array of hold IDs is also provided, information about
4286 each is returned as well. This information includes:
4287 1. a slightly easier to read version of holdable_formats
4288 2. attributes describing the set of format icons included
4289 in the set of desired, constituent records.
4292 {desc => 'Metarecord ID', type => 'number'},
4293 {desc => 'Context Org ID', type => 'number'},
4294 {desc => 'Hold ID List', type => 'array'},
4298 Stream of objects. The first will have a 'metarecord' key
4299 containing non-hold-specific metarecord information, subsequent
4300 responses will contain a 'hold' key containing hold-specific
4308 sub mr_hold_filter_attrs {
4309 my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4310 my $e = new_editor();
4312 # by default, return MR / hold attributes for all constituent
4313 # records with holdable copies. If there is a hard boundary,
4314 # though, limit to records with copies within the boundary,
4315 # since anything outside the boundary can never be held.
4318 $org_depth = $U->ou_ancestor_setting_value(
4319 $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4322 # get all org-scoped records w/ holdable copies for this metarecord
4323 my ($bre_ids) = $self->method_lookup(
4324 'open-ils.circ.holds.metarecord.filtered_records')->run(
4325 $mr_id, undef, $org_id, $org_depth);
4327 my $item_lang_attr = 'item_lang'; # configurable?
4328 my $format_attr = $e->retrieve_config_global_flag(
4329 'opac.metarecord.holds.format_attr')->value;
4331 # helper sub for fetching ccvms for a batch of record IDs
4332 sub get_batch_ccvms {
4333 my ($e, $attr, $bre_ids) = @_;
4334 return [] unless $bre_ids and @$bre_ids;
4335 my $vals = $e->search_metabib_record_attr_flat({
4339 return [] unless @$vals;
4340 return $e->search_config_coded_value_map({
4342 code => [map {$_->value} @$vals]
4346 my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4347 my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4352 formats => $formats,
4357 return unless $hold_ids;
4358 my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4359 $icon_attr = $icon_attr ? $icon_attr->value : '';
4361 for my $hold_id (@$hold_ids) {
4362 my $hold = $e->retrieve_action_hold_request($hold_id)
4363 or return $e->event;
4365 next unless $hold->hold_type eq 'M';
4375 # collect the ccvm's for the selected formats / language
4376 # (i.e. the holdable formats) on the MR.
4377 # this assumes a two-key structure for format / language,
4378 # though no assumption is made about the keys themselves.
4379 my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4381 my $format_vals = [];
4382 for my $val (values %$hformats) {
4383 # val is either a single ccvm or an array of them
4384 $val = [$val] unless ref $val eq 'ARRAY';
4385 for my $node (@$val) {
4386 push (@$lang_vals, $node->{_val})
4387 if $node->{_attr} eq $item_lang_attr;
4388 push (@$format_vals, $node->{_val})
4389 if $node->{_attr} eq $format_attr;
4393 # fetch the ccvm's for consistency with the {metarecord} blob
4394 $resp->{hold}{formats} = $e->search_config_coded_value_map({
4395 ctype => $format_attr, code => $format_vals});
4396 $resp->{hold}{langs} = $e->search_config_coded_value_map({
4397 ctype => $item_lang_attr, code => $lang_vals});
4399 # find all of the bib records within this metarcord whose
4400 # format / language match the holdable formats on the hold
4401 my ($bre_ids) = $self->method_lookup(
4402 'open-ils.circ.holds.metarecord.filtered_records')->run(
4403 $hold->target, $hold->holdable_formats,
4404 $hold->selection_ou, $hold->selection_depth);
4406 # now find all of the 'icon' attributes for the records
4407 $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4408 $client->respond($resp);
4414 __PACKAGE__->register_method(
4415 method => "copy_has_holds_count",
4416 api_name => "open-ils.circ.copy.has_holds_count",
4420 Returns the number of holds a paticular copy has
4423 { desc => 'Authentication Token', type => 'string'},
4424 { desc => 'Copy ID', type => 'number'}
4435 sub copy_has_holds_count {
4436 my( $self, $conn, $auth, $copyid ) = @_;
4437 my $e = new_editor(authtoken=>$auth);
4438 return $e->event unless $e->checkauth;
4440 if( $copyid && $copyid > 0 ) {
4441 my $meth = 'retrieve_action_has_holds_count';
4442 my $data = $e->$meth($copyid);
4444 return $data->count();