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'
1239 sub retrieve_hold_status {
1240 my($self, $client, $auth, $hold_id) = @_;
1242 my $e = new_editor(authtoken => $auth);
1243 return $e->event unless $e->checkauth;
1244 my $hold = $e->retrieve_action_hold_request($hold_id)
1245 or return $e->event;
1247 if( $e->requestor->id != $hold->usr ) {
1248 return $e->event unless $e->allowed('VIEW_HOLD');
1251 return _hold_status($e, $hold);
1257 if ($hold->cancel_time) {
1260 if ($U->is_true($hold->frozen) && !$hold->capture_time) {
1263 if ($hold->current_shelf_lib and $hold->current_shelf_lib ne $hold->pickup_lib) {
1266 return 1 unless $hold->current_copy;
1267 return 2 unless $hold->capture_time;
1269 my $copy = $hold->current_copy;
1270 unless( ref $copy ) {
1271 $copy = $e->retrieve_asset_copy($hold->current_copy)
1272 or return $e->event;
1275 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
1277 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
1279 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
1280 return 4 unless $hs_wait_interval;
1282 # if a hold_shelf_status_delay interval is defined and start_time plus
1283 # the interval is greater than now, consider the hold to be in the virtual
1284 # "on its way to the holds shelf" status. Return 5.
1286 my $transit = $e->search_action_hold_transit_copy({
1288 target_copy => $copy->id,
1289 dest_recv_time => {'!=' => undef},
1291 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
1292 $start_time = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time));
1293 my $end_time = $start_time->add(seconds => OpenSRF::Utils::interval_to_seconds($hs_wait_interval));
1295 return 5 if $end_time > DateTime->now;
1304 __PACKAGE__->register_method(
1305 method => "retrieve_hold_queue_stats",
1306 api_name => "open-ils.circ.hold.queue_stats.retrieve",
1308 desc => 'Returns summary data about the state of a hold',
1310 { desc => 'Authentication token', type => 'string'},
1311 { desc => 'Hold ID', type => 'number'},
1314 desc => q/Summary object with keys:
1315 total_holds : total holds in queue
1316 queue_position : current queue position
1317 potential_copies : number of potential copies for this hold
1318 estimated_wait : estimated wait time in days
1319 status : hold status
1320 -1 => error or unexpected state,
1321 1 => 'waiting for copy to become available',
1322 2 => 'waiting for copy capture',
1325 5 => 'hold-shelf-delay'
1332 sub retrieve_hold_queue_stats {
1333 my($self, $conn, $auth, $hold_id) = @_;
1334 my $e = new_editor(authtoken => $auth);
1335 return $e->event unless $e->checkauth;
1336 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
1337 if($e->requestor->id != $hold->usr) {
1338 return $e->event unless $e->allowed('VIEW_HOLD');
1340 return retrieve_hold_queue_status_impl($e, $hold);
1343 sub retrieve_hold_queue_status_impl {
1347 # The holds queue is defined as the distinct set of holds that share at
1348 # least one potential copy with the context hold, plus any holds that
1349 # share the same hold type and target. The latter part exists to
1350 # accomodate holds that currently have no potential copies
1351 my $q_holds = $e->json_query({
1353 # fetch cut_in_line and request_time since they're in the order_by
1354 # and we're asking for distinct values
1355 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1359 select => { ahcm => ['hold'] },
1364 'field' => 'target_copy',
1365 'fkey' => 'target_copy'
1369 where => { '+ahcm2' => { hold => $hold->id } },
1376 "field" => "cut_in_line",
1377 "transform" => "coalesce",
1379 "direction" => "desc"
1381 { "class" => "ahr", "field" => "request_time" }
1386 if (!@$q_holds) { # none? maybe we don't have a map ...
1387 $q_holds = $e->json_query({
1388 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1393 "field" => "cut_in_line",
1394 "transform" => "coalesce",
1396 "direction" => "desc"
1398 { "class" => "ahr", "field" => "request_time" }
1401 hold_type => $hold->hold_type,
1402 target => $hold->target,
1403 capture_time => undef,
1404 cancel_time => undef,
1406 {expire_time => undef },
1407 {expire_time => {'>' => 'now'}}
1415 for my $h (@$q_holds) {
1416 last if $h->{id} == $hold->id;
1420 my $hold_data = $e->json_query({
1422 acp => [ {column => 'id', transform => 'count', aggregate => 1, alias => 'count'} ],
1423 ccm => [ {column =>'avg_wait_time'} ]
1429 ccm => {type => 'left'}
1434 where => {'+ahcm' => {hold => $hold->id} }
1437 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1439 my $default_wait = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
1440 my $min_wait = $U->ou_ancestor_setting_value($user_org, 'circ.holds.min_estimated_wait_interval');
1441 $min_wait = OpenSRF::Utils::interval_to_seconds($min_wait || '0 seconds');
1442 $default_wait ||= '0 seconds';
1444 # Estimated wait time is the average wait time across the set
1445 # of potential copies, divided by the number of potential copies
1446 # times the queue position.
1448 my $combined_secs = 0;
1449 my $num_potentials = 0;
1451 for my $wait_data (@$hold_data) {
1452 my $count += $wait_data->{count};
1453 $combined_secs += $count *
1454 OpenSRF::Utils::interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
1455 $num_potentials += $count;
1458 my $estimated_wait = -1;
1460 if($num_potentials) {
1461 my $avg_wait = $combined_secs / $num_potentials;
1462 $estimated_wait = $qpos * ($avg_wait / $num_potentials);
1463 $estimated_wait = $min_wait if $estimated_wait < $min_wait and $estimated_wait != -1;
1467 total_holds => scalar(@$q_holds),
1468 queue_position => $qpos,
1469 potential_copies => $num_potentials,
1470 status => _hold_status( $e, $hold ),
1471 estimated_wait => int($estimated_wait)
1476 sub fetch_open_hold_by_current_copy {
1479 my $hold = $apputils->simplereq(
1481 'open-ils.cstore.direct.action.hold_request.search.atomic',
1482 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1483 return $hold->[0] if ref($hold);
1487 sub fetch_related_holds {
1490 return $apputils->simplereq(
1492 'open-ils.cstore.direct.action.hold_request.search.atomic',
1493 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1497 __PACKAGE__->register_method(
1498 method => "hold_pull_list",
1499 api_name => "open-ils.circ.hold_pull_list.retrieve",
1501 desc => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1502 'The location is determined by the login session.',
1504 { desc => 'Limit (optional)', type => 'number'},
1505 { desc => 'Offset (optional)', type => 'number'},
1508 desc => 'reference to a list of holds, or event on failure',
1513 __PACKAGE__->register_method(
1514 method => "hold_pull_list",
1515 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
1517 desc => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
1518 'The location is determined by the login session.',
1520 { desc => 'Limit (optional)', type => 'number'},
1521 { desc => 'Offset (optional)', type => 'number'},
1524 desc => 'reference to a list of holds, or event on failure',
1529 __PACKAGE__->register_method(
1530 method => "hold_pull_list",
1531 api_name => "open-ils.circ.hold_pull_list.retrieve.count",
1533 desc => 'Returns a count of holds that need to be "pulled" by a given location. ' .
1534 'The location is determined by the login session.',
1536 { desc => 'Limit (optional)', type => 'number'},
1537 { desc => 'Offset (optional)', type => 'number'},
1540 desc => 'Holds count (integer), or event on failure',
1546 __PACKAGE__->register_method(
1547 method => "hold_pull_list",
1549 # TODO: tag with api_level 2 once fully supported
1550 api_name => "open-ils.circ.hold_pull_list.fleshed.stream",
1552 desc => q/Returns a stream of fleshed holds that need to be
1553 "pulled" by a given location. The location is
1554 determined by the login session.
1555 This API calls always run in authoritative mode./,
1557 { desc => 'Limit (optional)', type => 'number'},
1558 { desc => 'Offset (optional)', type => 'number'},
1561 desc => 'Stream of holds holds, or event on failure',
1566 sub hold_pull_list {
1567 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1568 my( $reqr, $evt ) = $U->checkses($authtoken);
1569 return $evt if $evt;
1571 my $org = $reqr->ws_ou || $reqr->home_ou;
1572 # the perm locaiton shouldn't really matter here since holds
1573 # will exist all over and VIEW_HOLDS should be universal
1574 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1575 return $evt if $evt;
1577 if($self->api_name =~ /count/) {
1579 my $count = $U->storagereq(
1580 'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1581 $org, $limit, $offset );
1583 $logger->info("Grabbing pull list for org unit $org with $count items");
1586 } elsif( $self->api_name =~ /id_list/ ) {
1588 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1589 $org, $limit, $offset );
1591 } elsif ($self->api_name =~ /fleshed/) {
1593 my $ids = $U->storagereq(
1594 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1595 $org, $limit, $offset );
1597 my $e = new_editor(xact => 1, requestor => $reqr);
1598 $conn->respond(uber_hold_impl($e, $_, {flesh_acpl => 1})) for @$ids;
1600 $conn->respond_complete;
1605 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1606 $org, $limit, $offset );
1610 __PACKAGE__->register_method(
1611 method => "print_hold_pull_list",
1612 api_name => "open-ils.circ.hold_pull_list.print",
1614 desc => 'Returns an HTML-formatted holds pull list',
1616 { desc => 'Authtoken', type => 'string'},
1617 { desc => 'Org unit ID. Optional, defaults to workstation org unit', type => 'number'},
1620 desc => 'HTML string',
1626 sub print_hold_pull_list {
1627 my($self, $client, $auth, $org_id) = @_;
1629 my $e = new_editor(authtoken=>$auth);
1630 return $e->event unless $e->checkauth;
1632 $org_id = (defined $org_id) ? $org_id : $e->requestor->ws_ou;
1633 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1635 my $hold_ids = $U->storagereq(
1636 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1639 return undef unless @$hold_ids;
1641 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1643 # Holds will /NOT/ be in order after this ...
1644 my $holds = $e->search_action_hold_request({id => $hold_ids}, {substream => 1});
1645 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1647 # ... so we must resort.
1648 my $hold_map = +{map { $_->id => $_ } @$holds};
1649 my $sorted_holds = [];
1650 push @$sorted_holds, $hold_map->{$_} foreach @$hold_ids;
1652 return $U->fire_object_event(
1653 undef, "ahr.format.pull_list", $sorted_holds,
1654 $org_id, undef, undef, $client
1659 __PACKAGE__->register_method(
1660 method => "print_hold_pull_list_stream",
1662 api_name => "open-ils.circ.hold_pull_list.print.stream",
1664 desc => 'Returns a stream of fleshed holds',
1666 { desc => 'Authtoken', type => 'string'},
1667 { 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)',
1672 desc => 'A stream of fleshed holds',
1678 sub print_hold_pull_list_stream {
1679 my($self, $client, $auth, $params) = @_;
1681 my $e = new_editor(authtoken=>$auth);
1682 return $e->die_event unless $e->checkauth;
1684 delete($$params{org_id}) unless (int($$params{org_id}));
1685 delete($$params{limit}) unless (int($$params{limit}));
1686 delete($$params{offset}) unless (int($$params{offset}));
1687 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
1688 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
1689 $$params{chunk_size} ||= 10;
1690 $client->max_chunk_size($$params{chunk_size}) if ($client->can('max_chunk_size'));
1692 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
1693 return $e->die_event unless $e->allowed('VIEW_HOLD', $$params{org_id });
1696 if ($$params{sort} && @{ $$params{sort} }) {
1697 for my $s (@{ $$params{sort} }) {
1698 if ($s eq 'acplo.position') {
1700 "class" => "acplo", "field" => "position",
1701 "transform" => "coalesce", "params" => [999]
1703 } elsif ($s eq 'prefix') {
1704 push @$sort, {"class" => "acnp", "field" => "label_sortkey"};
1705 } elsif ($s eq 'call_number') {
1706 push @$sort, {"class" => "acn", "field" => "label_sortkey"};
1707 } elsif ($s eq 'suffix') {
1708 push @$sort, {"class" => "acns", "field" => "label_sortkey"};
1709 } elsif ($s eq 'request_time') {
1710 push @$sort, {"class" => "ahr", "field" => "request_time"};
1714 push @$sort, {"class" => "ahr", "field" => "request_time"};
1717 my $holds_ids = $e->json_query(
1719 "select" => {"ahr" => ["id"]},
1724 "fkey" => "current_copy",
1726 "circ_lib" => $$params{org_id}, "status" => [0,7]
1731 "fkey" => "call_number",
1745 "fkey" => "circ_lib",
1748 "location" => {"=" => {"+acp" => "location"}}
1757 "capture_time" => undef,
1758 "cancel_time" => undef,
1760 {"expire_time" => undef },
1761 {"expire_time" => {">" => "now"}}
1765 (@$sort ? (order_by => $sort) : ()),
1766 ($$params{limit} ? (limit => $$params{limit}) : ()),
1767 ($$params{offset} ? (offset => $$params{offset}) : ())
1768 }, {"substream" => 1}
1769 ) or return $e->die_event;
1771 $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
1774 for my $hid (@$holds_ids) {
1775 push @chunk, $e->retrieve_action_hold_request([
1779 "ahr" => ["usr", "current_copy"],
1781 "acp" => ["location", "call_number", "parts"],
1782 "acn" => ["record","prefix","suffix"]
1787 if (@chunk >= $$params{chunk_size}) {
1788 $client->respond( \@chunk );
1792 $client->respond_complete( \@chunk ) if (@chunk);
1799 __PACKAGE__->register_method(
1800 method => 'fetch_hold_notify',
1801 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
1804 Returns a list of hold notification objects based on hold id.
1805 @param authtoken The loggin session key
1806 @param holdid The id of the hold whose notifications we want to retrieve
1807 @return An array of hold notification objects, event on error.
1811 sub fetch_hold_notify {
1812 my( $self, $conn, $authtoken, $holdid ) = @_;
1813 my( $requestor, $evt ) = $U->checkses($authtoken);
1814 return $evt if $evt;
1815 my ($hold, $patron);
1816 ($hold, $evt) = $U->fetch_hold($holdid);
1817 return $evt if $evt;
1818 ($patron, $evt) = $U->fetch_user($hold->usr);
1819 return $evt if $evt;
1821 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
1822 return $evt if $evt;
1824 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
1825 return $U->cstorereq(
1826 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
1830 __PACKAGE__->register_method(
1831 method => 'create_hold_notify',
1832 api_name => 'open-ils.circ.hold_notification.create',
1834 Creates a new hold notification object
1835 @param authtoken The login session key
1836 @param notification The hold notification object to create
1837 @return ID of the new object on success, Event on error
1841 sub create_hold_notify {
1842 my( $self, $conn, $auth, $note ) = @_;
1843 my $e = new_editor(authtoken=>$auth, xact=>1);
1844 return $e->die_event unless $e->checkauth;
1846 my $hold = $e->retrieve_action_hold_request($note->hold)
1847 or return $e->die_event;
1848 my $patron = $e->retrieve_actor_user($hold->usr)
1849 or return $e->die_event;
1851 return $e->die_event unless
1852 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
1854 $note->notify_staff($e->requestor->id);
1855 $e->create_action_hold_notification($note) or return $e->die_event;
1860 __PACKAGE__->register_method(
1861 method => 'create_hold_note',
1862 api_name => 'open-ils.circ.hold_note.create',
1864 Creates a new hold request note object
1865 @param authtoken The login session key
1866 @param note The hold note object to create
1867 @return ID of the new object on success, Event on error
1871 sub create_hold_note {
1872 my( $self, $conn, $auth, $note ) = @_;
1873 my $e = new_editor(authtoken=>$auth, xact=>1);
1874 return $e->die_event unless $e->checkauth;
1876 my $hold = $e->retrieve_action_hold_request($note->hold)
1877 or return $e->die_event;
1878 my $patron = $e->retrieve_actor_user($hold->usr)
1879 or return $e->die_event;
1881 return $e->die_event unless
1882 $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
1884 $e->create_action_hold_request_note($note) or return $e->die_event;
1889 __PACKAGE__->register_method(
1890 method => 'reset_hold',
1891 api_name => 'open-ils.circ.hold.reset',
1893 Un-captures and un-targets a hold, essentially returning
1894 it to the state it was in directly after it was placed,
1895 then attempts to re-target the hold
1896 @param authtoken The login session key
1897 @param holdid The id of the hold
1903 my( $self, $conn, $auth, $holdid ) = @_;
1905 my ($hold, $evt) = $U->fetch_hold($holdid);
1906 return $evt if $evt;
1907 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1908 return $evt if $evt;
1909 $evt = _reset_hold($self, $reqr, $hold);
1910 return $evt if $evt;
1915 __PACKAGE__->register_method(
1916 method => 'reset_hold_batch',
1917 api_name => 'open-ils.circ.hold.reset.batch'
1920 sub reset_hold_batch {
1921 my($self, $conn, $auth, $hold_ids) = @_;
1923 my $e = new_editor(authtoken => $auth);
1924 return $e->event unless $e->checkauth;
1926 for my $hold_id ($hold_ids) {
1928 my $hold = $e->retrieve_action_hold_request(
1929 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
1930 or return $e->event;
1932 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1933 _reset_hold($self, $e->requestor, $hold);
1941 my ($self, $reqr, $hold) = @_;
1943 my $e = new_editor(xact =>1, requestor => $reqr);
1945 $logger->info("reseting hold ".$hold->id);
1947 my $hid = $hold->id;
1949 if( $hold->capture_time and $hold->current_copy ) {
1951 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1952 or return $e->die_event;
1954 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1955 $logger->info("setting copy to status 'reshelving' on hold retarget");
1956 $copy->status(OILS_COPY_STATUS_RESHELVING);
1957 $copy->editor($e->requestor->id);
1958 $copy->edit_date('now');
1959 $e->update_asset_copy($copy) or return $e->die_event;
1961 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1963 # We don't want the copy to remain "in transit"
1964 $copy->status(OILS_COPY_STATUS_RESHELVING);
1965 $logger->warn("! reseting hold [$hid] that is in transit");
1966 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
1969 my $trans = $e->retrieve_action_transit_copy($transid);
1971 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1972 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
1973 $logger->info("Transit abort completed with result $evt");
1974 unless ("$evt" eq 1) {
1983 $hold->clear_capture_time;
1984 $hold->clear_current_copy;
1985 $hold->clear_shelf_time;
1986 $hold->clear_shelf_expire_time;
1987 $hold->clear_current_shelf_lib;
1989 $e->update_action_hold_request($hold) or return $e->die_event;
1993 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1999 __PACKAGE__->register_method(
2000 method => 'fetch_open_title_holds',
2001 api_name => 'open-ils.circ.open_holds.retrieve',
2003 Returns a list ids of un-fulfilled holds for a given title id
2004 @param authtoken The login session key
2005 @param id the id of the item whose holds we want to retrieve
2006 @param type The hold type - M, T, I, V, C, F, R
2010 sub fetch_open_title_holds {
2011 my( $self, $conn, $auth, $id, $type, $org ) = @_;
2012 my $e = new_editor( authtoken => $auth );
2013 return $e->event unless $e->checkauth;
2016 $org ||= $e->requestor->ws_ou;
2018 # return $e->search_action_hold_request(
2019 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
2021 # XXX make me return IDs in the future ^--
2022 my $holds = $e->search_action_hold_request(
2025 cancel_time => undef,
2027 fulfillment_time => undef
2031 flesh_hold_transits($holds);
2036 sub flesh_hold_transits {
2038 for my $hold ( @$holds ) {
2040 $apputils->simplereq(
2042 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
2043 { hold => $hold->id },
2044 { order_by => { ahtc => 'id desc' }, limit => 1 }
2050 sub flesh_hold_notices {
2051 my( $holds, $e ) = @_;
2052 $e ||= new_editor();
2054 for my $hold (@$holds) {
2055 my $notices = $e->search_action_hold_notification(
2057 { hold => $hold->id },
2058 { order_by => { anh => 'notify_time desc' } },
2063 $hold->notify_count(scalar(@$notices));
2065 my $n = $e->retrieve_action_hold_notification($$notices[0])
2066 or return $e->event;
2067 $hold->notify_time($n->notify_time);
2073 __PACKAGE__->register_method(
2074 method => 'fetch_captured_holds',
2075 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
2079 Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
2080 @param authtoken The login session key
2081 @param org The org id of the location in question
2082 @param match_copy A specific copy to limit to
2086 __PACKAGE__->register_method(
2087 method => 'fetch_captured_holds',
2088 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
2092 Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
2093 @param authtoken The login session key
2094 @param org The org id of the location in question
2095 @param match_copy A specific copy to limit to
2099 __PACKAGE__->register_method(
2100 method => 'fetch_captured_holds',
2101 api_name => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
2105 Returns list ids of shelf-expired un-fulfilled holds for a given title id
2106 @param authtoken The login session key
2107 @param org The org id of the location in question
2108 @param match_copy A specific copy to limit to
2112 __PACKAGE__->register_method(
2113 method => 'fetch_captured_holds',
2115 'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
2119 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2120 for a given shelf lib
2124 __PACKAGE__->register_method(
2125 method => 'fetch_captured_holds',
2127 'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
2131 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2132 for a given shelf lib
2137 sub fetch_captured_holds {
2138 my( $self, $conn, $auth, $org, $match_copy ) = @_;
2140 my $e = new_editor(authtoken => $auth);
2141 return $e->die_event unless $e->checkauth;
2142 return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
2144 $org ||= $e->requestor->ws_ou;
2146 my $current_copy = { '!=' => undef };
2147 $current_copy = { '=' => $match_copy } if $match_copy;
2150 select => { alhr => ['id'] },
2155 fkey => 'current_copy'
2160 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
2162 capture_time => { "!=" => undef },
2163 current_copy => $current_copy,
2164 fulfillment_time => undef,
2165 current_shelf_lib => $org
2169 if($self->api_name =~ /expired/) {
2170 $query->{'where'}->{'+alhr'}->{'-or'} = {
2171 shelf_expire_time => { '<' => 'today'},
2172 cancel_time => { '!=' => undef },
2175 my $hold_ids = $e->json_query( $query );
2177 if ($self->api_name =~ /wrong_shelf/) {
2178 # fetch holds whose current_shelf_lib is $org, but whose pickup
2179 # lib is some other org unit. Ignore already-retrieved holds.
2181 pickup_lib_changed_on_shelf_holds(
2182 $e, $org, [map {$_->{id}} @$hold_ids]);
2183 # match the layout of other items in $hold_ids
2184 push (@$hold_ids, {id => $_}) for @$wrong_shelf;
2188 for my $hold_id (@$hold_ids) {
2189 if($self->api_name =~ /id_list/) {
2190 $conn->respond($hold_id->{id});
2194 $e->retrieve_action_hold_request([
2198 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
2199 order_by => {anh => 'notify_time desc'}
2209 __PACKAGE__->register_method(
2210 method => "print_expired_holds_stream",
2211 api_name => "open-ils.circ.captured_holds.expired.print.stream",
2215 sub print_expired_holds_stream {
2216 my ($self, $client, $auth, $params) = @_;
2218 # No need to check specific permissions: we're going to call another method
2219 # that will do that.
2220 my $e = new_editor("authtoken" => $auth);
2221 return $e->die_event unless $e->checkauth;
2223 delete($$params{org_id}) unless (int($$params{org_id}));
2224 delete($$params{limit}) unless (int($$params{limit}));
2225 delete($$params{offset}) unless (int($$params{offset}));
2226 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2227 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2228 $$params{chunk_size} ||= 10;
2229 $client->max_chunk_size($$params{chunk_size});
2231 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2233 my @hold_ids = $self->method_lookup(
2234 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
2235 )->run($auth, $params->{"org_id"});
2240 } elsif (defined $U->event_code($hold_ids[0])) {
2242 return $hold_ids[0];
2245 $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
2248 my @hid_chunk = splice @hold_ids, 0, $params->{"chunk_size"};
2250 my $result_chunk = $e->json_query({
2252 "acp" => ["barcode"],
2254 first_given_name second_given_name family_name alias
2263 "field" => "id", "fkey" => "current_copy",
2266 "field" => "id", "fkey" => "call_number",
2269 "field" => "id", "fkey" => "record"
2273 "acpl" => {"field" => "id", "fkey" => "location"}
2276 "au" => {"field" => "id", "fkey" => "usr"}
2279 "where" => {"+ahr" => {"id" => \@hid_chunk}}
2280 }) or return $e->die_event;
2281 $client->respond($result_chunk);
2288 __PACKAGE__->register_method(
2289 method => "check_title_hold_batch",
2290 api_name => "open-ils.circ.title_hold.is_possible.batch",
2293 desc => '@see open-ils.circ.title_hold.is_possible.batch',
2295 { desc => 'Authentication token', type => 'string'},
2296 { desc => 'Array of Hash of named parameters', type => 'array'},
2299 desc => 'Array of response objects',
2305 sub check_title_hold_batch {
2306 my($self, $client, $authtoken, $param_list, $oargs) = @_;
2307 foreach (@$param_list) {
2308 my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
2309 $client->respond($res);
2315 __PACKAGE__->register_method(
2316 method => "check_title_hold",
2317 api_name => "open-ils.circ.title_hold.is_possible",
2319 desc => 'Determines if a hold were to be placed by a given user, ' .
2320 'whether or not said hold would have any potential copies to fulfill it.' .
2321 'The named paramaters of the second argument include: ' .
2322 'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
2323 'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
2325 { desc => 'Authentication token', type => 'string'},
2326 { desc => 'Hash of named parameters', type => 'object'},
2329 desc => 'List of new message IDs (empty if none)',
2335 =head3 check_title_hold (token, hash)
2337 The named fields in the hash are:
2339 patronid - ID of the hold recipient (required)
2340 depth - hold range depth (default 0)
2341 pickup_lib - destination for hold, fallback value for selection_ou
2342 selection_ou - ID of org_unit establishing hard and soft hold boundary settings
2343 issuanceid - ID of the issuance to be held, required for Issuance level hold
2344 partid - ID of the monograph part to be held, required for monograph part level hold
2345 titleid - ID (BRN) of the title to be held, required for Title level hold
2346 volume_id - required for Volume level hold
2347 copy_id - required for Copy level hold
2348 mrid - required for Meta-record level hold
2349 hold_type - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record (default "T")
2351 All key/value pairs are passed on to do_possibility_checks.
2355 # FIXME: better params checking. what other params are required, if any?
2356 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
2357 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
2358 # used in conditionals, where it may be undefined, causing a warning.
2359 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
2361 sub check_title_hold {
2362 my( $self, $client, $authtoken, $params ) = @_;
2363 my $e = new_editor(authtoken=>$authtoken);
2364 return $e->event unless $e->checkauth;
2366 my %params = %$params;
2367 my $depth = $params{depth} || 0;
2368 my $selection_ou = $params{selection_ou} || $params{pickup_lib};
2369 my $oargs = $params{oargs} || {};
2371 if($oargs->{events}) {
2372 @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
2376 my $patron = $e->retrieve_actor_user($params{patronid})
2377 or return $e->event;
2379 if( $e->requestor->id ne $patron->id ) {
2380 return $e->event unless
2381 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
2384 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
2386 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
2387 or return $e->event;
2389 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
2390 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
2393 my $return_depth = $hard_boundary; # default depth to return on success
2394 if(defined $soft_boundary and $depth < $soft_boundary) {
2395 # work up the tree and as soon as we find a potential copy, use that depth
2396 # also, make sure we don't go past the hard boundary if it exists
2398 # our min boundary is the greater of user-specified boundary or hard boundary
2399 my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
2400 $hard_boundary : $depth;
2402 my $depth = $soft_boundary;
2403 while($depth >= $min_depth) {
2404 $logger->info("performing hold possibility check with soft boundary $depth");
2405 @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
2407 $return_depth = $depth;
2412 } elsif(defined $hard_boundary and $depth < $hard_boundary) {
2413 # there is no soft boundary, enforce the hard boundary if it exists
2414 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
2415 @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
2417 # no boundaries defined, fall back to user specifed boundary or no boundary
2418 $logger->info("performing hold possibility check with no boundary");
2419 @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
2422 my $place_unfillable = 0;
2423 $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
2428 "depth" => $return_depth,
2429 "local_avail" => $status[1]
2431 } elsif ($status[2]) {
2432 my $n = scalar @{$status[2]};
2433 return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2435 return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2441 sub do_possibility_checks {
2442 my($e, $patron, $request_lib, $depth, %params) = @_;
2444 my $issuanceid = $params{issuanceid} || "";
2445 my $partid = $params{partid} || "";
2446 my $titleid = $params{titleid} || "";
2447 my $volid = $params{volume_id};
2448 my $copyid = $params{copy_id};
2449 my $mrid = $params{mrid} || "";
2450 my $pickup_lib = $params{pickup_lib};
2451 my $hold_type = $params{hold_type} || 'T';
2452 my $selection_ou = $params{selection_ou} || $pickup_lib;
2453 my $holdable_formats = $params{holdable_formats};
2454 my $oargs = $params{oargs} || {};
2461 if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
2463 return $e->event unless $copy = $e->retrieve_asset_copy($copyid);
2464 return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
2465 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2467 return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
2468 return verify_copy_for_hold(
2469 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
2472 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2474 return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
2475 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2477 return _check_volume_hold_is_possible(
2478 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2481 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
2483 return _check_title_hold_is_possible(
2484 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
2487 } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
2489 return _check_issuance_hold_is_possible(
2490 $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2493 } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
2495 return _check_monopart_hold_is_possible(
2496 $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2499 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2501 # pasing undef as the depth to filtered_records causes the depth
2502 # of the selection_ou to be used, which is not what we want here.
2505 my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
2507 for my $rec (@$recs) {
2508 @status = _check_title_hold_is_possible(
2509 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
2515 # else { Unrecognized hold_type ! } # FIXME: return error? or 0?
2518 sub MR_filter_records {
2525 my $opac_visible = shift;
2527 my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
2528 return $U->storagereq(
2529 'open-ils.storage.metarecord.filtered_records.atomic',
2530 $m, $f, $org_at_depth, $opac_visible
2533 __PACKAGE__->register_method(
2534 method => 'MR_filter_records',
2535 api_name => 'open-ils.circ.holds.metarecord.filtered_records',
2540 sub create_ranged_org_filter {
2541 my($e, $selection_ou, $depth) = @_;
2543 # find the orgs from which this hold may be fulfilled,
2544 # based on the selection_ou and depth
2546 my $top_org = $e->search_actor_org_unit([
2547 {parent_ou => undef},
2548 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
2551 return () if $depth == $top_org->ou_type->depth;
2553 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
2554 %org_filter = (circ_lib => []);
2555 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
2557 $logger->info("hold org filter at depth $depth and selection_ou ".
2558 "$selection_ou created list of @{$org_filter{circ_lib}}");
2564 sub _check_title_hold_is_possible {
2565 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
2566 # $holdable_formats is now unused. We pre-filter the MR's records.
2568 my $e = new_editor();
2569 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2571 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2572 my $copies = $e->json_query(
2574 select => { acp => ['id', 'circ_lib'] },
2579 fkey => 'call_number',
2580 filter => { record => $titleid }
2584 filter => { holdable => 't', deleted => 'f' },
2587 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' },
2588 acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2592 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2593 '+acpm' => { target_copy => undef } # ignore part-linked copies
2598 $logger->info("title possible found ".scalar(@$copies)." potential copies");
2602 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2603 "payload" => {"fail_part" => "no_ultimate_items"}
2608 # -----------------------------------------------------------------------
2609 # sort the copies into buckets based on their circ_lib proximity to
2610 # the patron's home_ou.
2611 # -----------------------------------------------------------------------
2613 my $home_org = $patron->home_ou;
2614 my $req_org = $request_lib->id;
2616 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2618 $prox_cache{$home_org} =
2619 $e->search_actor_org_unit_proximity({from_org => $home_org})
2620 unless $prox_cache{$home_org};
2621 my $home_prox = $prox_cache{$home_org};
2624 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2625 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2627 my @keys = sort { $a <=> $b } keys %buckets;
2630 if( $home_org ne $req_org ) {
2631 # -----------------------------------------------------------------------
2632 # shove the copies close to the request_lib into the primary buckets
2633 # directly before the farthest away copies. That way, they are not
2634 # given priority, but they are checked before the farthest copies.
2635 # -----------------------------------------------------------------------
2636 $prox_cache{$req_org} =
2637 $e->search_actor_org_unit_proximity({from_org => $req_org})
2638 unless $prox_cache{$req_org};
2639 my $req_prox = $prox_cache{$req_org};
2642 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2643 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2645 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2646 my $new_key = $highest_key - 0.5; # right before the farthest prox
2647 my @keys2 = sort { $a <=> $b } keys %buckets2;
2648 for my $key (@keys2) {
2649 last if $key >= $highest_key;
2650 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2654 @keys = sort { $a <=> $b } keys %buckets;
2659 my $age_protect_only = 0;
2660 OUTER: for my $key (@keys) {
2661 my @cps = @{$buckets{$key}};
2663 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2665 for my $copyid (@cps) {
2667 next if $seen{$copyid};
2668 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2669 my $copy = $e->retrieve_asset_copy($copyid);
2670 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2672 unless($title) { # grab the title if we don't already have it
2673 my $vol = $e->retrieve_asset_call_number(
2674 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2675 $title = $vol->record;
2678 @status = verify_copy_for_hold(
2679 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2681 $age_protect_only ||= $status[3];
2682 last OUTER if $status[0];
2686 $status[3] = $age_protect_only;
2690 sub _check_issuance_hold_is_possible {
2691 my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2693 my $e = new_editor();
2694 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2696 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2697 my $copies = $e->json_query(
2699 select => { acp => ['id', 'circ_lib'] },
2705 filter => { issuance => $issuanceid }
2709 filter => { holdable => 't', deleted => 'f' },
2712 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
2716 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
2722 $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
2726 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
2727 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2732 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2733 "payload" => {"fail_part" => "no_ultimate_items"}
2741 # -----------------------------------------------------------------------
2742 # sort the copies into buckets based on their circ_lib proximity to
2743 # the patron's home_ou.
2744 # -----------------------------------------------------------------------
2746 my $home_org = $patron->home_ou;
2747 my $req_org = $request_lib->id;
2749 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2751 $prox_cache{$home_org} =
2752 $e->search_actor_org_unit_proximity({from_org => $home_org})
2753 unless $prox_cache{$home_org};
2754 my $home_prox = $prox_cache{$home_org};
2757 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2758 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2760 my @keys = sort { $a <=> $b } keys %buckets;
2763 if( $home_org ne $req_org ) {
2764 # -----------------------------------------------------------------------
2765 # shove the copies close to the request_lib into the primary buckets
2766 # directly before the farthest away copies. That way, they are not
2767 # given priority, but they are checked before the farthest copies.
2768 # -----------------------------------------------------------------------
2769 $prox_cache{$req_org} =
2770 $e->search_actor_org_unit_proximity({from_org => $req_org})
2771 unless $prox_cache{$req_org};
2772 my $req_prox = $prox_cache{$req_org};
2775 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2776 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2778 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2779 my $new_key = $highest_key - 0.5; # right before the farthest prox
2780 my @keys2 = sort { $a <=> $b } keys %buckets2;
2781 for my $key (@keys2) {
2782 last if $key >= $highest_key;
2783 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2787 @keys = sort { $a <=> $b } keys %buckets;
2792 my $age_protect_only = 0;
2793 OUTER: for my $key (@keys) {
2794 my @cps = @{$buckets{$key}};
2796 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2798 for my $copyid (@cps) {
2800 next if $seen{$copyid};
2801 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2802 my $copy = $e->retrieve_asset_copy($copyid);
2803 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2805 unless($title) { # grab the title if we don't already have it
2806 my $vol = $e->retrieve_asset_call_number(
2807 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2808 $title = $vol->record;
2811 @status = verify_copy_for_hold(
2812 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2814 $age_protect_only ||= $status[3];
2815 last OUTER if $status[0];
2820 if (!defined($empty_ok)) {
2821 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
2822 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2825 return (1,0) if ($empty_ok);
2827 $status[3] = $age_protect_only;
2831 sub _check_monopart_hold_is_possible {
2832 my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2834 my $e = new_editor();
2835 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2837 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2838 my $copies = $e->json_query(
2840 select => { acp => ['id', 'circ_lib'] },
2844 field => 'target_copy',
2846 filter => { part => $partid }
2850 filter => { holdable => 't', deleted => 'f' },
2853 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
2857 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
2863 $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
2867 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
2868 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2873 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2874 "payload" => {"fail_part" => "no_ultimate_items"}
2882 # -----------------------------------------------------------------------
2883 # sort the copies into buckets based on their circ_lib proximity to
2884 # the patron's home_ou.
2885 # -----------------------------------------------------------------------
2887 my $home_org = $patron->home_ou;
2888 my $req_org = $request_lib->id;
2890 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
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};
2898 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2899 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2901 my @keys = sort { $a <=> $b } keys %buckets;
2904 if( $home_org ne $req_org ) {
2905 # -----------------------------------------------------------------------
2906 # shove the copies close to the request_lib into the primary buckets
2907 # directly before the farthest away copies. That way, they are not
2908 # given priority, but they are checked before the farthest copies.
2909 # -----------------------------------------------------------------------
2910 $prox_cache{$req_org} =
2911 $e->search_actor_org_unit_proximity({from_org => $req_org})
2912 unless $prox_cache{$req_org};
2913 my $req_prox = $prox_cache{$req_org};
2916 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2917 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2919 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2920 my $new_key = $highest_key - 0.5; # right before the farthest prox
2921 my @keys2 = sort { $a <=> $b } keys %buckets2;
2922 for my $key (@keys2) {
2923 last if $key >= $highest_key;
2924 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2928 @keys = sort { $a <=> $b } keys %buckets;
2933 my $age_protect_only = 0;
2934 OUTER: for my $key (@keys) {
2935 my @cps = @{$buckets{$key}};
2937 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2939 for my $copyid (@cps) {
2941 next if $seen{$copyid};
2942 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2943 my $copy = $e->retrieve_asset_copy($copyid);
2944 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2946 unless($title) { # grab the title if we don't already have it
2947 my $vol = $e->retrieve_asset_call_number(
2948 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2949 $title = $vol->record;
2952 @status = verify_copy_for_hold(
2953 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2955 $age_protect_only ||= $status[3];
2956 last OUTER if $status[0];
2961 if (!defined($empty_ok)) {
2962 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
2963 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2966 return (1,0) if ($empty_ok);
2968 $status[3] = $age_protect_only;
2973 sub _check_volume_hold_is_possible {
2974 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2975 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
2976 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
2977 $logger->info("checking possibility of volume hold for volume ".$vol->id);
2979 my $filter_copies = [];
2980 for my $copy (@$copies) {
2981 # ignore part-mapped copies for regular volume level holds
2982 push(@$filter_copies, $copy) unless
2983 new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
2985 $copies = $filter_copies;
2990 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2991 "payload" => {"fail_part" => "no_ultimate_items"}
2997 my $age_protect_only = 0;
2998 for my $copy ( @$copies ) {
2999 @status = verify_copy_for_hold(
3000 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
3001 $age_protect_only ||= $status[3];
3004 $status[3] = $age_protect_only;
3010 sub verify_copy_for_hold {
3011 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
3012 # $oargs should be undef unless we're overriding.
3013 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
3014 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3017 requestor => $requestor,
3020 title_descriptor => $title->fixed_fields,
3021 pickup_lib => $pickup_lib,
3022 request_lib => $request_lib,
3024 show_event_list => 1
3028 # Check for override permissions on events.
3029 if ($oargs && $permitted && scalar @$permitted) {
3030 # Remove the events from permitted that we can override.
3031 if ($oargs->{events}) {
3032 foreach my $evt (@{$oargs->{events}}) {
3033 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3036 # Now, we handle the override all case by checking remaining
3037 # events against override permissions.
3038 if (scalar @$permitted && $oargs->{all}) {
3039 # Pre-set events and failed members of oargs to empty
3040 # arrays, if they are not set, yet.
3041 $oargs->{events} = [] unless ($oargs->{events});
3042 $oargs->{failed} = [] unless ($oargs->{failed});
3043 # When we're done with these checks, we swap permitted
3044 # with a reference to @disallowed.
3045 my @disallowed = ();
3046 foreach my $evt (@{$permitted}) {
3047 # Check if we've already seen the event in this
3048 # session and it failed.
3049 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3050 push(@disallowed, $evt);
3052 # We have to check if the requestor has the
3053 # override permission.
3055 # AppUtils::check_user_perms returns the perm if
3056 # the user doesn't have it, undef if they do.
3057 if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3058 push(@disallowed, $evt);
3059 push(@{$oargs->{failed}}, $evt->{textcode});
3061 push(@{$oargs->{events}}, $evt->{textcode});
3065 $permitted = \@disallowed;
3069 my $age_protect_only = 0;
3070 if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3071 $age_protect_only = 1;
3075 (not scalar @$permitted), # true if permitted is an empty arrayref
3076 ( # XXX This test is of very dubious value; someone should figure
3077 # out what if anything is checking this value
3078 ($copy->circ_lib == $pickup_lib) and
3079 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3088 sub find_nearest_permitted_hold {
3091 my $editor = shift; # CStoreEditor object
3092 my $copy = shift; # copy to target
3093 my $user = shift; # staff
3094 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3096 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3098 my $bc = $copy->barcode;
3100 # find any existing holds that already target this copy
3101 my $old_holds = $editor->search_action_hold_request(
3102 { current_copy => $copy->id,
3103 cancel_time => undef,
3104 capture_time => undef
3108 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3110 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3111 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3113 my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3115 # the nearest_hold API call now needs this
3116 $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3117 unless ref $copy->call_number;
3119 # search for what should be the best holds for this copy to fulfill
3120 my $best_holds = $U->storagereq(
3121 "open-ils.storage.action.hold_request.nearest_hold.atomic",
3122 $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3124 # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3126 for my $holdid (@$old_holds) {
3127 next unless $holdid;
3128 push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3132 unless(@$best_holds) {
3133 $logger->info("circulator: no suitable holds found for copy $bc");
3134 return (undef, $evt);
3140 # for each potential hold, we have to run the permit script
3141 # to make sure the hold is actually permitted.
3144 for my $holdid (@$best_holds) {
3145 next unless $holdid;
3146 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3148 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3149 # Force and recall holds bypass all rules
3150 if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3154 my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3155 my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3157 $reqr_cache{$hold->requestor} = $reqr;
3158 $org_cache{$hold->request_lib} = $rlib;
3160 # see if this hold is permitted
3161 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3163 patron_id => $hold->usr,
3166 pickup_lib => $hold->pickup_lib,
3167 request_lib => $rlib,
3179 unless( $best_hold ) { # no "good" permitted holds were found
3181 $logger->info("circulator: no suitable holds found for copy $bc");
3182 return (undef, $evt);
3185 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3187 # indicate a permitted hold was found
3188 return $best_hold if $check_only;
3190 # we've found a permitted hold. we need to "grab" the copy
3191 # to prevent re-targeted holds (next part) from re-grabbing the copy
3192 $best_hold->current_copy($copy->id);
3193 $editor->update_action_hold_request($best_hold)
3194 or return (undef, $editor->event);
3199 # re-target any other holds that already target this copy
3200 for my $old_hold (@$old_holds) {
3201 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3202 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3203 $old_hold->id." after a better hold [".$best_hold->id."] was found");
3204 $old_hold->clear_current_copy;
3205 $old_hold->clear_prev_check_time;
3206 $editor->update_action_hold_request($old_hold)
3207 or return (undef, $editor->event);
3208 push(@retarget, $old_hold->id);
3211 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3219 __PACKAGE__->register_method(
3220 method => 'all_rec_holds',
3221 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3225 my( $self, $conn, $auth, $title_id, $args ) = @_;
3227 my $e = new_editor(authtoken=>$auth);
3228 $e->checkauth or return $e->event;
3229 $e->allowed('VIEW_HOLD') or return $e->event;
3232 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
3233 $args->{cancel_time} = undef;
3236 metarecord_holds => []
3238 , volume_holds => []
3240 , recall_holds => []
3243 , issuance_holds => []
3246 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3248 $resp->{metarecord_holds} = $e->search_action_hold_request(
3249 { hold_type => OILS_HOLD_TYPE_METARECORD,
3250 target => $mr_map->metarecord,
3256 $resp->{title_holds} = $e->search_action_hold_request(
3258 hold_type => OILS_HOLD_TYPE_TITLE,
3259 target => $title_id,
3263 my $parts = $e->search_biblio_monograph_part(
3269 $resp->{part_holds} = $e->search_action_hold_request(
3271 hold_type => OILS_HOLD_TYPE_MONOPART,
3277 my $subs = $e->search_serial_subscription(
3278 { record_entry => $title_id }, {idlist=>1});
3281 my $issuances = $e->search_serial_issuance(
3282 {subscription => $subs}, {idlist=>1}
3286 $resp->{issuance_holds} = $e->search_action_hold_request(
3288 hold_type => OILS_HOLD_TYPE_ISSUANCE,
3289 target => $issuances,
3296 my $vols = $e->search_asset_call_number(
3297 { record => $title_id, deleted => 'f' }, {idlist=>1});
3299 return $resp unless @$vols;
3301 $resp->{volume_holds} = $e->search_action_hold_request(
3303 hold_type => OILS_HOLD_TYPE_VOLUME,
3308 my $copies = $e->search_asset_copy(
3309 { call_number => $vols, deleted => 'f' }, {idlist=>1});
3311 return $resp unless @$copies;
3313 $resp->{copy_holds} = $e->search_action_hold_request(
3315 hold_type => OILS_HOLD_TYPE_COPY,
3320 $resp->{recall_holds} = $e->search_action_hold_request(
3322 hold_type => OILS_HOLD_TYPE_RECALL,
3327 $resp->{force_holds} = $e->search_action_hold_request(
3329 hold_type => OILS_HOLD_TYPE_FORCE,
3341 __PACKAGE__->register_method(
3342 method => 'uber_hold',
3344 api_name => 'open-ils.circ.hold.details.retrieve'
3348 my($self, $client, $auth, $hold_id, $args) = @_;
3349 my $e = new_editor(authtoken=>$auth);
3350 $e->checkauth or return $e->event;
3351 return uber_hold_impl($e, $hold_id, $args);
3354 __PACKAGE__->register_method(
3355 method => 'batch_uber_hold',
3358 api_name => 'open-ils.circ.hold.details.batch.retrieve'
3361 sub batch_uber_hold {
3362 my($self, $client, $auth, $hold_ids, $args) = @_;
3363 my $e = new_editor(authtoken=>$auth);
3364 $e->checkauth or return $e->event;
3365 $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3369 sub uber_hold_impl {
3370 my($e, $hold_id, $args) = @_;
3373 my $hold = $e->retrieve_action_hold_request(
3378 flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
3381 ) or return $e->event;
3383 if($hold->usr->id ne $e->requestor->id) {
3384 # caller is asking for someone else's hold
3385 $e->allowed('VIEW_HOLD') or return $e->event;
3386 $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3387 [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3390 # caller is asking for own hold, but may not have permission to view staff notes
3391 unless($e->allowed('VIEW_HOLD')) {
3392 $hold->notes( # filter out any staff notes (unless marked as public)
3393 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3397 my $user = $hold->usr;
3398 $hold->usr($user->id);
3401 my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args->{suppress_mvr});
3403 flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3404 flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3406 my $details = retrieve_hold_queue_status_impl($e, $hold);
3411 ($copy ? (copy => $copy) : ()),
3412 ($volume ? (volume => $volume) : ()),
3413 ($issuance ? (issuance => $issuance) : ()),
3414 ($part ? (part => $part) : ()),
3415 ($args->{include_bre} ? (bre => $bre) : ()),
3416 ($args->{suppress_mvr} ? () : (mvr => $mvr)),
3420 $resp->{copy}->location(
3421 $e->retrieve_asset_copy_location($resp->{copy}->location))
3422 if $resp->{copy} and $args->{flesh_acpl};
3424 unless($args->{suppress_patron_details}) {
3425 my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3426 $resp->{patron_first} = $user->first_given_name,
3427 $resp->{patron_last} = $user->family_name,
3428 $resp->{patron_barcode} = $card->barcode,
3429 $resp->{patron_alias} = $user->alias,
3437 # -----------------------------------------------------
3438 # Returns the MVR object that represents what the
3440 # -----------------------------------------------------
3442 my( $e, $hold, $no_mvr ) = @_;
3450 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3451 my $mr = $e->retrieve_metabib_metarecord($hold->target)
3452 or return $e->event;
3453 $tid = $mr->master_record;
3455 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3456 $tid = $hold->target;
3458 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3459 $volume = $e->retrieve_asset_call_number($hold->target)
3460 or return $e->event;
3461 $tid = $volume->record;
3463 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3464 $issuance = $e->retrieve_serial_issuance([
3466 {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3467 ]) or return $e->event;
3469 $tid = $issuance->subscription->record_entry;
3471 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3472 $part = $e->retrieve_biblio_monograph_part([
3474 ]) or return $e->event;
3476 $tid = $part->record;
3478 } 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 ) {
3479 $copy = $e->retrieve_asset_copy([
3481 {flesh => 1, flesh_fields => {acp => ['call_number']}}
3482 ]) or return $e->event;
3484 $volume = $copy->call_number;
3485 $tid = $volume->record;
3488 if(!$copy and ref $hold->current_copy ) {
3489 $copy = $hold->current_copy;
3490 $hold->current_copy($copy->id);
3493 if(!$volume and $copy) {
3494 $volume = $e->retrieve_asset_call_number($copy->call_number);
3497 # TODO return metarcord mvr for M holds
3498 my $title = $e->retrieve_biblio_record_entry($tid);
3499 return ( ($no_mvr) ? undef : $U->record_to_mvr($title), $volume, $copy, $issuance, $part, $title );
3502 __PACKAGE__->register_method(
3503 method => 'clear_shelf_cache',
3504 api_name => 'open-ils.circ.hold.clear_shelf.get_cache',
3508 Returns the holds processed with the given cache key
3513 sub clear_shelf_cache {
3514 my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3515 my $e = new_editor(authtoken => $auth, xact => 1);
3516 return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3519 $client->max_chunk_size($chunk_size) if ($client->can('max_chunk_size'));
3521 my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3524 $logger->info("no hold data found in cache"); # XXX TODO return event
3530 foreach (keys %$hold_data) {
3531 $maximum += scalar(@{ $hold_data->{$_} });
3533 $client->respond({"maximum" => $maximum, "progress" => 0});
3535 for my $action (sort keys %$hold_data) {
3536 while (@{$hold_data->{$action}}) {
3537 my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3539 my $result_chunk = $e->json_query({
3541 "acp" => ["barcode"],
3543 first_given_name second_given_name family_name alias
3546 "acnp" => [{column => "label", alias => "prefix"}],
3547 "acns" => [{column => "label", alias => "suffix"}],
3555 "field" => "id", "fkey" => "current_copy",
3558 "field" => "id", "fkey" => "call_number",
3561 "field" => "id", "fkey" => "record"
3564 "field" => "id", "fkey" => "prefix"
3567 "field" => "id", "fkey" => "suffix"
3571 "acpl" => {"field" => "id", "fkey" => "location"}
3574 "au" => {"field" => "id", "fkey" => "usr"}
3577 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3578 }, {"substream" => 1}) or return $e->die_event;
3582 +{"action" => $action, "hold_details" => $_}
3593 __PACKAGE__->register_method(
3594 method => 'clear_shelf_process',
3596 api_name => 'open-ils.circ.hold.clear_shelf.process',
3599 1. Find all holds that have expired on the holds shelf
3601 3. If a clear-shelf status is configured, put targeted copies into this status
3602 4. Divide copies into 3 groups: items to transit, items to reshelve, and items
3603 that are needed for holds. No subsequent action is taken on the holds
3604 or items after grouping.
3609 sub clear_shelf_process {
3610 my($self, $client, $auth, $org_id, $match_copy, $chunk_size) = @_;
3612 my $e = new_editor(authtoken=>$auth);
3613 $e->checkauth or return $e->die_event;
3614 my $cache = OpenSRF::Utils::Cache->new('global');
3616 $org_id ||= $e->requestor->ws_ou;
3617 $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
3619 my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
3621 my @hold_ids = $self->method_lookup(
3622 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
3623 )->run($auth, $org_id, $match_copy);
3628 my @canceled_holds; # newly canceled holds
3629 $chunk_size ||= 25; # chunked status updates
3630 $client->max_chunk_size($chunk_size) if ($client->can('max_chunk_size'));
3633 for my $hold_id (@hold_ids) {
3635 $logger->info("Clear shelf processing hold $hold_id");
3637 my $hold = $e->retrieve_action_hold_request([
3640 flesh_fields => {ahr => ['current_copy']}
3644 if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
3645 $hold->cancel_time('now');
3646 $hold->cancel_cause(2); # Hold Shelf expiration
3647 $e->update_action_hold_request($hold) or return $e->die_event;
3648 push(@canceled_holds, $hold_id);
3651 my $copy = $hold->current_copy;
3653 if($copy_status or $copy_status == 0) {
3654 # if a clear-shelf copy status is defined, update the copy
3655 $copy->status($copy_status);
3656 $copy->edit_date('now');
3657 $copy->editor($e->requestor->id);
3658 $e->update_asset_copy($copy) or return $e->die_event;
3661 push(@holds, $hold);
3662 $client->respond({maximum => scalar(@holds), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
3671 pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
3674 for my $hold (@holds) {
3676 my $copy = $hold->current_copy;
3677 my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
3679 if($alt_hold and !$match_copy) {
3681 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
3683 } elsif($copy->circ_lib != $e->requestor->ws_ou) {
3685 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
3689 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
3693 my $cache_key = md5_hex(time . $$ . rand());
3694 $logger->info("clear_shelf_cache: storing under $cache_key");
3695 $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours. configurable?
3697 # tell the client we're done
3698 $client->respond_complete({cache_key => $cache_key});
3701 # fire off the hold cancelation trigger and wait for response so don't flood the service
3703 # refetch the holds to pick up the caclulated cancel_time,
3704 # which may be needed by Action/Trigger
3706 my $updated_holds = [];
3707 $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
3710 $U->create_events_for_hook(
3711 'hold_request.cancel.expire_holds_shelf',
3712 $_, $org_id, undef, undef, 1) for @$updated_holds;
3715 # tell the client we're done
3716 $client->respond_complete;
3720 # returns IDs for holds that are on the holds shelf but
3721 # have had their pickup_libs change while on the shelf.
3722 sub pickup_lib_changed_on_shelf_holds {
3725 my $ignore_holds = shift;
3726 $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
3729 select => { alhr => ['id'] },
3734 fkey => 'current_copy'
3739 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
3741 capture_time => { "!=" => undef },
3742 fulfillment_time => undef,
3743 current_shelf_lib => $org_id,
3744 pickup_lib => {'!=' => {'+alhr' => 'current_shelf_lib'}}
3749 $query->{where}->{'+alhr'}->{id} =
3750 {'not in' => $ignore_holds} if @$ignore_holds;
3752 my $hold_ids = $e->json_query($query);
3753 return [ map { $_->{id} } @$hold_ids ];
3756 __PACKAGE__->register_method(
3757 method => 'usr_hold_summary',
3758 api_name => 'open-ils.circ.holds.user_summary',
3760 Returns a summary of holds statuses for a given user
3764 sub usr_hold_summary {
3765 my($self, $conn, $auth, $user_id) = @_;
3767 my $e = new_editor(authtoken=>$auth);
3768 $e->checkauth or return $e->event;
3769 $e->allowed('VIEW_HOLD') or return $e->event;
3771 my $holds = $e->search_action_hold_request(
3774 fulfillment_time => undef,
3775 cancel_time => undef,
3779 my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
3780 $summary{_hold_status($e, $_)} += 1 for @$holds;
3786 __PACKAGE__->register_method(
3787 method => 'hold_has_copy_at',
3788 api_name => 'open-ils.circ.hold.has_copy_at',
3791 'Returns the ID of the found copy and name of the shelving location if there is ' .
3792 'an available copy at the specified org unit. Returns empty hash otherwise. ' .
3793 'The anticipated use for this method is to determine whether an item is ' .
3794 'available at the library where the user is placing the hold (or, alternatively, '.
3795 'at the pickup library) to encourage bypassing the hold placement and just ' .
3796 'checking out the item.' ,
3798 { desc => 'Authentication Token', type => 'string' },
3799 { desc => 'Method Arguments. Options include: hold_type, hold_target, org_unit. '
3800 . 'hold_type is the hold type code (T, V, C, M, ...). '
3801 . 'hold_target is the identifier of the hold target object. '
3802 . 'org_unit is org unit ID.',
3807 desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
3813 sub hold_has_copy_at {
3814 my($self, $conn, $auth, $args) = @_;
3816 my $e = new_editor(authtoken=>$auth);
3817 $e->checkauth or return $e->event;
3819 my $hold_type = $$args{hold_type};
3820 my $hold_target = $$args{hold_target};
3821 my $org_unit = $$args{org_unit};
3824 select => {acp => ['id'], acpl => ['name']},
3829 filter => { holdable => 't', deleted => 'f' },
3832 ccs => {field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3835 where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit, status => [0,7]}},
3839 if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
3841 $query->{where}->{'+acp'}->{id} = $hold_target;
3843 } elsif($hold_type eq 'V') {
3845 $query->{where}->{'+acp'}->{call_number} = $hold_target;
3847 } elsif($hold_type eq 'P') {
3849 $query->{from}->{acp}->{acpm} = {
3850 field => 'target_copy',
3852 filter => {part => $hold_target},
3855 } elsif($hold_type eq 'I') {
3857 $query->{from}->{acp}->{sitem} = {
3860 filter => {issuance => $hold_target},
3863 } elsif($hold_type eq 'T') {
3865 $query->{from}->{acp}->{acn} = {
3867 fkey => 'call_number',
3871 filter => {id => $hold_target},
3879 $query->{from}->{acp}->{acn} = {
3881 fkey => 'call_number',
3890 filter => {metarecord => $hold_target},
3898 my $res = $e->json_query($query)->[0] or return {};
3899 return {copy => $res->{id}, location => $res->{name}} if $res;
3903 # returns true if the user already has an item checked out
3904 # that could be used to fulfill the requested hold.
3905 sub hold_item_is_checked_out {
3906 my($e, $user_id, $hold_type, $hold_target) = @_;
3909 select => {acp => ['id']},
3910 from => {acp => {}},
3914 in => { # copies for circs the user has checked out
3915 select => {circ => ['target_copy']},
3919 checkin_time => undef,
3921 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
3922 {stop_fines => undef}
3932 if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
3934 $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
3936 } elsif($hold_type eq 'V') {
3938 $query->{where}->{'+acp'}->{call_number} = $hold_target;
3940 } elsif($hold_type eq 'P') {
3942 $query->{from}->{acp}->{acpm} = {
3943 field => 'target_copy',
3945 filter => {part => $hold_target},
3948 } elsif($hold_type eq 'I') {
3950 $query->{from}->{acp}->{sitem} = {
3953 filter => {issuance => $hold_target},
3956 } elsif($hold_type eq 'T') {
3958 $query->{from}->{acp}->{acn} = {
3960 fkey => 'call_number',
3964 filter => {id => $hold_target},
3972 $query->{from}->{acp}->{acn} = {
3974 fkey => 'call_number',
3983 filter => {metarecord => $hold_target},
3991 return $e->json_query($query)->[0];
3994 __PACKAGE__->register_method(
3995 method => 'change_hold_title',
3996 api_name => 'open-ils.circ.hold.change_title',
3999 Updates all title level holds targeting the specified bibs to point a new bib./,
4001 { desc => 'Authentication Token', type => 'string' },
4002 { desc => 'New Target Bib Id', type => 'number' },
4003 { desc => 'Old Target Bib Ids', type => 'array' },
4005 return => { desc => '1 on success' }
4009 __PACKAGE__->register_method(
4010 method => 'change_hold_title_for_specific_holds',
4011 api_name => 'open-ils.circ.hold.change_title.specific_holds',
4014 Updates specified holds to target new bib./,
4016 { desc => 'Authentication Token', type => 'string' },
4017 { desc => 'New Target Bib Id', type => 'number' },
4018 { desc => 'Holds Ids for holds to update', type => 'array' },
4020 return => { desc => '1 on success' }
4025 sub change_hold_title {
4026 my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4028 my $e = new_editor(authtoken=>$auth, xact=>1);
4029 return $e->die_event unless $e->checkauth;
4031 my $holds = $e->search_action_hold_request(
4034 cancel_time => undef,
4035 fulfillment_time => undef,
4041 flesh_fields => { ahr => ['usr'] }
4047 for my $hold (@$holds) {
4048 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4049 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4050 $hold->target( $new_bib_id );
4051 $e->update_action_hold_request($hold) or return $e->die_event;
4056 _reset_hold($self, $e->requestor, $_) for @$holds;
4061 sub change_hold_title_for_specific_holds {
4062 my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4064 my $e = new_editor(authtoken=>$auth, xact=>1);
4065 return $e->die_event unless $e->checkauth;
4067 my $holds = $e->search_action_hold_request(
4070 cancel_time => undef,
4071 fulfillment_time => undef,
4077 flesh_fields => { ahr => ['usr'] }
4083 for my $hold (@$holds) {
4084 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4085 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4086 $hold->target( $new_bib_id );
4087 $e->update_action_hold_request($hold) or return $e->die_event;
4092 _reset_hold($self, $e->requestor, $_) for @$holds;
4097 __PACKAGE__->register_method(
4098 method => 'rec_hold_count',
4099 api_name => 'open-ils.circ.bre.holds.count',
4101 desc => q/Returns the total number of holds that target the
4102 selected bib record or its associated copies and call_numbers/,
4104 { desc => 'Bib ID', type => 'number' },
4105 { desc => q/Optional arguments. Supported arguments include:
4106 "pickup_lib_descendant" -> limit holds to those whose pickup
4107 library is equal to or is a child of the provided org unit/,
4111 return => {desc => 'Hold count', type => 'number'}
4115 __PACKAGE__->register_method(
4116 method => 'rec_hold_count',
4117 api_name => 'open-ils.circ.mmr.holds.count',
4119 desc => q/Returns the total number of holds that target the
4120 selected metarecord or its associated copies, call_numbers, and bib records/,
4122 { desc => 'Metarecord ID', type => 'number' },
4124 return => {desc => 'Hold count', type => 'number'}
4128 # XXX Need to add type I holds to these counts
4129 sub rec_hold_count {
4130 my($self, $conn, $target_id, $args) = @_;
4137 filter => {metarecord => $target_id}
4144 filter => { id => $target_id },
4149 if($self->api_name =~ /mmr/) {
4150 delete $bre_join->{bre}->{filter};
4151 $bre_join->{bre}->{join} = $mmr_join;
4157 fkey => 'call_number',
4163 select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4167 cancel_time => undef,
4168 fulfillment_time => undef,
4172 hold_type => [qw/C F R/],
4175 select => {acp => ['id']},
4176 from => { acp => $cn_join }
4186 select => {acn => ['id']},
4187 from => {acn => $bre_join}
4197 select => {bmp => ['id']},
4198 from => {bmp => $bre_join}
4206 target => $target_id
4214 if($self->api_name =~ /mmr/) {
4215 $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4220 select => {bre => ['id']},
4221 from => {bre => $mmr_join}
4227 $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4230 target => $target_id
4236 if (my $pld = $args->{pickup_lib_descendant}) {
4238 my $top_ou = new_editor()->search_actor_org_unit(
4239 {parent_ou => undef}
4240 )->[0]; # XXX Assumes single root node. Not alone in this...
4242 $query->{where}->{'+ahr'}->{pickup_lib} = {
4244 select => {aou => [{
4246 transform => 'actor.org_unit_descendants',
4247 result_field => 'id'
4250 where => {id => $pld}
4252 } if ($pld != $top_ou->id);
4256 return new_editor()->json_query($query)->[0]->{count};
4259 # A helper function to calculate a hold's expiration time at a given
4260 # org_unit. Takes the org_unit as an argument and returns either the
4261 # hold expire time as an ISO8601 string or undef if there is no hold
4262 # expiration interval set for the subject ou.
4263 sub calculate_expire_time
4266 my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4268 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
4269 return $U->epoch2ISO8601($date->epoch);
4275 __PACKAGE__->register_method(
4276 method => 'mr_hold_filter_attrs',
4277 api_name => 'open-ils.circ.mmr.holds.filters',
4282 Returns the set of available formats and languages for the
4283 constituent records of the provided metarcord.
4284 If an array of hold IDs is also provided, information about
4285 each is returned as well. This information includes:
4286 1. a slightly easier to read version of holdable_formats
4287 2. attributes describing the set of format icons included
4288 in the set of desired, constituent records.
4291 {desc => 'Metarecord ID', type => 'number'},
4292 {desc => 'Context Org ID', type => 'number'},
4293 {desc => 'Hold ID List', type => 'array'},
4297 Stream of objects. The first will have a 'metarecord' key
4298 containing non-hold-specific metarecord information, subsequent
4299 responses will contain a 'hold' key containing hold-specific
4307 sub mr_hold_filter_attrs {
4308 my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4309 my $e = new_editor();
4311 # by default, return MR / hold attributes for all constituent
4312 # records with holdable copies. If there is a hard boundary,
4313 # though, limit to records with copies within the boundary,
4314 # since anything outside the boundary can never be held.
4317 $org_depth = $U->ou_ancestor_setting_value(
4318 $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4321 # get all org-scoped records w/ holdable copies for this metarecord
4322 my ($bre_ids) = $self->method_lookup(
4323 'open-ils.circ.holds.metarecord.filtered_records')->run(
4324 $mr_id, undef, $org_id, $org_depth);
4326 my $item_lang_attr = 'item_lang'; # configurable?
4327 my $format_attr = $e->retrieve_config_global_flag(
4328 'opac.metarecord.holds.format_attr')->value;
4330 # helper sub for fetching ccvms for a batch of record IDs
4331 sub get_batch_ccvms {
4332 my ($e, $attr, $bre_ids) = @_;
4333 return [] unless $bre_ids and @$bre_ids;
4334 my $vals = $e->search_metabib_record_attr_flat({
4338 return [] unless @$vals;
4339 return $e->search_config_coded_value_map({
4341 code => [map {$_->value} @$vals]
4345 my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4346 my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4351 formats => $formats,
4356 return unless $hold_ids;
4357 my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4358 $icon_attr = $icon_attr ? $icon_attr->value : '';
4360 for my $hold_id (@$hold_ids) {
4361 my $hold = $e->retrieve_action_hold_request($hold_id)
4362 or return $e->event;
4364 next unless $hold->hold_type eq 'M';
4374 # collect the ccvm's for the selected formats / language
4375 # (i.e. the holdable formats) on the MR.
4376 # this assumes a two-key structure for format / language,
4377 # though no assumption is made about the keys themselves.
4378 my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4380 my $format_vals = [];
4381 for my $val (values %$hformats) {
4382 # val is either a single ccvm or an array of them
4383 $val = [$val] unless ref $val eq 'ARRAY';
4384 for my $node (@$val) {
4385 push (@$lang_vals, $node->{_val})
4386 if $node->{_attr} eq $item_lang_attr;
4387 push (@$format_vals, $node->{_val})
4388 if $node->{_attr} eq $format_attr;
4392 # fetch the ccvm's for consistency with the {metarecord} blob
4393 $resp->{hold}{formats} = $e->search_config_coded_value_map({
4394 ctype => $format_attr, code => $format_vals});
4395 $resp->{hold}{langs} = $e->search_config_coded_value_map({
4396 ctype => $item_lang_attr, code => $lang_vals});
4398 # find all of the bib records within this metarcord whose
4399 # format / language match the holdable formats on the hold
4400 my ($bre_ids) = $self->method_lookup(
4401 'open-ils.circ.holds.metarecord.filtered_records')->run(
4402 $hold->target, $hold->holdable_formats,
4403 $hold->selection_ou, $hold->selection_depth);
4405 # now find all of the 'icon' attributes for the records
4406 $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4407 $client->respond($resp);
4413 __PACKAGE__->register_method(
4414 method => "copy_has_holds_count",
4415 api_name => "open-ils.circ.copy.has_holds_count",
4419 Returns the number of holds a paticular copy has
4422 { desc => 'Authentication Token', type => 'string'},
4423 { desc => 'Copy ID', type => 'number'}
4434 sub copy_has_holds_count {
4435 my( $self, $conn, $auth, $copyid ) = @_;
4436 my $e = new_editor(authtoken=>$auth);
4437 return $e->event unless $e->checkauth;
4439 if( $copyid && $copyid > 0 ) {
4440 my $meth = 'retrieve_action_has_holds_count';
4441 my $data = $e->$meth($copyid);
4443 return $data->count();