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({hold => $hold->id})->[0];
1287 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
1288 $start_time = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time));
1289 my $end_time = $start_time->add(seconds => OpenSRF::Utils::interval_to_seconds($hs_wait_interval));
1291 return 5 if $end_time > DateTime->now;
1300 __PACKAGE__->register_method(
1301 method => "retrieve_hold_queue_stats",
1302 api_name => "open-ils.circ.hold.queue_stats.retrieve",
1304 desc => 'Returns summary data about the state of a hold',
1306 { desc => 'Authentication token', type => 'string'},
1307 { desc => 'Hold ID', type => 'number'},
1310 desc => q/Summary object with keys:
1311 total_holds : total holds in queue
1312 queue_position : current queue position
1313 potential_copies : number of potential copies for this hold
1314 estimated_wait : estimated wait time in days
1315 status : hold status
1316 -1 => error or unexpected state,
1317 1 => 'waiting for copy to become available',
1318 2 => 'waiting for copy capture',
1321 5 => 'hold-shelf-delay'
1328 sub retrieve_hold_queue_stats {
1329 my($self, $conn, $auth, $hold_id) = @_;
1330 my $e = new_editor(authtoken => $auth);
1331 return $e->event unless $e->checkauth;
1332 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
1333 if($e->requestor->id != $hold->usr) {
1334 return $e->event unless $e->allowed('VIEW_HOLD');
1336 return retrieve_hold_queue_status_impl($e, $hold);
1339 sub retrieve_hold_queue_status_impl {
1343 # The holds queue is defined as the distinct set of holds that share at
1344 # least one potential copy with the context hold, plus any holds that
1345 # share the same hold type and target. The latter part exists to
1346 # accomodate holds that currently have no potential copies
1347 my $q_holds = $e->json_query({
1349 # fetch cut_in_line and request_time since they're in the order_by
1350 # and we're asking for distinct values
1351 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1355 select => { ahcm => ['hold'] },
1360 'field' => 'target_copy',
1361 'fkey' => 'target_copy'
1365 where => { '+ahcm2' => { hold => $hold->id } },
1372 "field" => "cut_in_line",
1373 "transform" => "coalesce",
1375 "direction" => "desc"
1377 { "class" => "ahr", "field" => "request_time" }
1382 if (!@$q_holds) { # none? maybe we don't have a map ...
1383 $q_holds = $e->json_query({
1384 select => {ahr => ['id', 'cut_in_line', 'request_time']},
1389 "field" => "cut_in_line",
1390 "transform" => "coalesce",
1392 "direction" => "desc"
1394 { "class" => "ahr", "field" => "request_time" }
1397 hold_type => $hold->hold_type,
1398 target => $hold->target,
1399 capture_time => undef,
1400 cancel_time => undef,
1402 {expire_time => undef },
1403 {expire_time => {'>' => 'now'}}
1411 for my $h (@$q_holds) {
1412 last if $h->{id} == $hold->id;
1416 my $hold_data = $e->json_query({
1418 acp => [ {column => 'id', transform => 'count', aggregate => 1, alias => 'count'} ],
1419 ccm => [ {column =>'avg_wait_time'} ]
1425 ccm => {type => 'left'}
1430 where => {'+ahcm' => {hold => $hold->id} }
1433 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
1435 my $default_wait = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
1436 my $min_wait = $U->ou_ancestor_setting_value($user_org, 'circ.holds.min_estimated_wait_interval');
1437 $min_wait = OpenSRF::Utils::interval_to_seconds($min_wait || '0 seconds');
1438 $default_wait ||= '0 seconds';
1440 # Estimated wait time is the average wait time across the set
1441 # of potential copies, divided by the number of potential copies
1442 # times the queue position.
1444 my $combined_secs = 0;
1445 my $num_potentials = 0;
1447 for my $wait_data (@$hold_data) {
1448 my $count += $wait_data->{count};
1449 $combined_secs += $count *
1450 OpenSRF::Utils::interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
1451 $num_potentials += $count;
1454 my $estimated_wait = -1;
1456 if($num_potentials) {
1457 my $avg_wait = $combined_secs / $num_potentials;
1458 $estimated_wait = $qpos * ($avg_wait / $num_potentials);
1459 $estimated_wait = $min_wait if $estimated_wait < $min_wait and $estimated_wait != -1;
1463 total_holds => scalar(@$q_holds),
1464 queue_position => $qpos,
1465 potential_copies => $num_potentials,
1466 status => _hold_status( $e, $hold ),
1467 estimated_wait => int($estimated_wait)
1472 sub fetch_open_hold_by_current_copy {
1475 my $hold = $apputils->simplereq(
1477 'open-ils.cstore.direct.action.hold_request.search.atomic',
1478 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1479 return $hold->[0] if ref($hold);
1483 sub fetch_related_holds {
1486 return $apputils->simplereq(
1488 'open-ils.cstore.direct.action.hold_request.search.atomic',
1489 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
1493 __PACKAGE__->register_method(
1494 method => "hold_pull_list",
1495 api_name => "open-ils.circ.hold_pull_list.retrieve",
1497 desc => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
1498 'The location is determined by the login session.',
1500 { desc => 'Limit (optional)', type => 'number'},
1501 { desc => 'Offset (optional)', type => 'number'},
1504 desc => 'reference to a list of holds, or event on failure',
1509 __PACKAGE__->register_method(
1510 method => "hold_pull_list",
1511 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
1513 desc => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
1514 'The location is determined by the login session.',
1516 { desc => 'Limit (optional)', type => 'number'},
1517 { desc => 'Offset (optional)', type => 'number'},
1520 desc => 'reference to a list of holds, or event on failure',
1525 __PACKAGE__->register_method(
1526 method => "hold_pull_list",
1527 api_name => "open-ils.circ.hold_pull_list.retrieve.count",
1529 desc => 'Returns a count of holds that need to be "pulled" by a given location. ' .
1530 'The location is determined by the login session.',
1532 { desc => 'Limit (optional)', type => 'number'},
1533 { desc => 'Offset (optional)', type => 'number'},
1536 desc => 'Holds count (integer), or event on failure',
1542 __PACKAGE__->register_method(
1543 method => "hold_pull_list",
1545 # TODO: tag with api_level 2 once fully supported
1546 api_name => "open-ils.circ.hold_pull_list.fleshed.stream",
1548 desc => q/Returns a stream of fleshed holds that need to be
1549 "pulled" by a given location. The location is
1550 determined by the login session.
1551 This API calls always run in authoritative mode./,
1553 { desc => 'Limit (optional)', type => 'number'},
1554 { desc => 'Offset (optional)', type => 'number'},
1557 desc => 'Stream of holds holds, or event on failure',
1562 sub hold_pull_list {
1563 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1564 my( $reqr, $evt ) = $U->checkses($authtoken);
1565 return $evt if $evt;
1567 my $org = $reqr->ws_ou || $reqr->home_ou;
1568 # the perm locaiton shouldn't really matter here since holds
1569 # will exist all over and VIEW_HOLDS should be universal
1570 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1571 return $evt if $evt;
1573 if($self->api_name =~ /count/) {
1575 my $count = $U->storagereq(
1576 'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1577 $org, $limit, $offset );
1579 $logger->info("Grabbing pull list for org unit $org with $count items");
1582 } elsif( $self->api_name =~ /id_list/ ) {
1584 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1585 $org, $limit, $offset );
1587 } elsif ($self->api_name =~ /fleshed/) {
1589 my $ids = $U->storagereq(
1590 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1591 $org, $limit, $offset );
1593 my $e = new_editor(xact => 1, requestor => $reqr);
1594 $conn->respond(uber_hold_impl($e, $_, {flesh_acpl => 1})) for @$ids;
1596 $conn->respond_complete;
1601 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1602 $org, $limit, $offset );
1606 __PACKAGE__->register_method(
1607 method => "print_hold_pull_list",
1608 api_name => "open-ils.circ.hold_pull_list.print",
1610 desc => 'Returns an HTML-formatted holds pull list',
1612 { desc => 'Authtoken', type => 'string'},
1613 { desc => 'Org unit ID. Optional, defaults to workstation org unit', type => 'number'},
1616 desc => 'HTML string',
1622 sub print_hold_pull_list {
1623 my($self, $client, $auth, $org_id) = @_;
1625 my $e = new_editor(authtoken=>$auth);
1626 return $e->event unless $e->checkauth;
1628 $org_id = (defined $org_id) ? $org_id : $e->requestor->ws_ou;
1629 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1631 my $hold_ids = $U->storagereq(
1632 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1635 return undef unless @$hold_ids;
1637 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1639 # Holds will /NOT/ be in order after this ...
1640 my $holds = $e->search_action_hold_request({id => $hold_ids}, {substream => 1});
1641 $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
1643 # ... so we must resort.
1644 my $hold_map = +{map { $_->id => $_ } @$holds};
1645 my $sorted_holds = [];
1646 push @$sorted_holds, $hold_map->{$_} foreach @$hold_ids;
1648 return $U->fire_object_event(
1649 undef, "ahr.format.pull_list", $sorted_holds,
1650 $org_id, undef, undef, $client
1655 __PACKAGE__->register_method(
1656 method => "print_hold_pull_list_stream",
1658 api_name => "open-ils.circ.hold_pull_list.print.stream",
1660 desc => 'Returns a stream of fleshed holds',
1662 { desc => 'Authtoken', type => 'string'},
1663 { 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)',
1668 desc => 'A stream of fleshed holds',
1674 sub print_hold_pull_list_stream {
1675 my($self, $client, $auth, $params) = @_;
1677 my $e = new_editor(authtoken=>$auth);
1678 return $e->die_event unless $e->checkauth;
1680 delete($$params{org_id}) unless (int($$params{org_id}));
1681 delete($$params{limit}) unless (int($$params{limit}));
1682 delete($$params{offset}) unless (int($$params{offset}));
1683 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
1684 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
1685 $$params{chunk_size} ||= 10;
1687 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
1688 return $e->die_event unless $e->allowed('VIEW_HOLD', $$params{org_id });
1691 if ($$params{sort} && @{ $$params{sort} }) {
1692 for my $s (@{ $$params{sort} }) {
1693 if ($s eq 'acplo.position') {
1695 "class" => "acplo", "field" => "position",
1696 "transform" => "coalesce", "params" => [999]
1698 } elsif ($s eq 'prefix') {
1699 push @$sort, {"class" => "acnp", "field" => "label_sortkey"};
1700 } elsif ($s eq 'call_number') {
1701 push @$sort, {"class" => "acn", "field" => "label_sortkey"};
1702 } elsif ($s eq 'suffix') {
1703 push @$sort, {"class" => "acns", "field" => "label_sortkey"};
1704 } elsif ($s eq 'request_time') {
1705 push @$sort, {"class" => "ahr", "field" => "request_time"};
1709 push @$sort, {"class" => "ahr", "field" => "request_time"};
1712 my $holds_ids = $e->json_query(
1714 "select" => {"ahr" => ["id"]},
1719 "fkey" => "current_copy",
1721 "circ_lib" => $$params{org_id}, "status" => [0,7]
1726 "fkey" => "call_number",
1740 "fkey" => "circ_lib",
1743 "location" => {"=" => {"+acp" => "location"}}
1752 "capture_time" => undef,
1753 "cancel_time" => undef,
1755 {"expire_time" => undef },
1756 {"expire_time" => {">" => "now"}}
1760 (@$sort ? (order_by => $sort) : ()),
1761 ($$params{limit} ? (limit => $$params{limit}) : ()),
1762 ($$params{offset} ? (offset => $$params{offset}) : ())
1763 }, {"substream" => 1}
1764 ) or return $e->die_event;
1766 $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
1769 for my $hid (@$holds_ids) {
1770 push @chunk, $e->retrieve_action_hold_request([
1774 "ahr" => ["usr", "current_copy"],
1776 "acp" => ["location", "call_number", "parts"],
1777 "acn" => ["record","prefix","suffix"]
1782 if (@chunk >= $$params{chunk_size}) {
1783 $client->respond( \@chunk );
1787 $client->respond_complete( \@chunk ) if (@chunk);
1794 __PACKAGE__->register_method(
1795 method => 'fetch_hold_notify',
1796 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
1799 Returns a list of hold notification objects based on hold id.
1800 @param authtoken The loggin session key
1801 @param holdid The id of the hold whose notifications we want to retrieve
1802 @return An array of hold notification objects, event on error.
1806 sub fetch_hold_notify {
1807 my( $self, $conn, $authtoken, $holdid ) = @_;
1808 my( $requestor, $evt ) = $U->checkses($authtoken);
1809 return $evt if $evt;
1810 my ($hold, $patron);
1811 ($hold, $evt) = $U->fetch_hold($holdid);
1812 return $evt if $evt;
1813 ($patron, $evt) = $U->fetch_user($hold->usr);
1814 return $evt if $evt;
1816 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
1817 return $evt if $evt;
1819 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
1820 return $U->cstorereq(
1821 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
1825 __PACKAGE__->register_method(
1826 method => 'create_hold_notify',
1827 api_name => 'open-ils.circ.hold_notification.create',
1829 Creates a new hold notification object
1830 @param authtoken The login session key
1831 @param notification The hold notification object to create
1832 @return ID of the new object on success, Event on error
1836 sub create_hold_notify {
1837 my( $self, $conn, $auth, $note ) = @_;
1838 my $e = new_editor(authtoken=>$auth, xact=>1);
1839 return $e->die_event unless $e->checkauth;
1841 my $hold = $e->retrieve_action_hold_request($note->hold)
1842 or return $e->die_event;
1843 my $patron = $e->retrieve_actor_user($hold->usr)
1844 or return $e->die_event;
1846 return $e->die_event unless
1847 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
1849 $note->notify_staff($e->requestor->id);
1850 $e->create_action_hold_notification($note) or return $e->die_event;
1855 __PACKAGE__->register_method(
1856 method => 'create_hold_note',
1857 api_name => 'open-ils.circ.hold_note.create',
1859 Creates a new hold request note object
1860 @param authtoken The login session key
1861 @param note The hold note object to create
1862 @return ID of the new object on success, Event on error
1866 sub create_hold_note {
1867 my( $self, $conn, $auth, $note ) = @_;
1868 my $e = new_editor(authtoken=>$auth, xact=>1);
1869 return $e->die_event unless $e->checkauth;
1871 my $hold = $e->retrieve_action_hold_request($note->hold)
1872 or return $e->die_event;
1873 my $patron = $e->retrieve_actor_user($hold->usr)
1874 or return $e->die_event;
1876 return $e->die_event unless
1877 $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
1879 $e->create_action_hold_request_note($note) or return $e->die_event;
1884 __PACKAGE__->register_method(
1885 method => 'reset_hold',
1886 api_name => 'open-ils.circ.hold.reset',
1888 Un-captures and un-targets a hold, essentially returning
1889 it to the state it was in directly after it was placed,
1890 then attempts to re-target the hold
1891 @param authtoken The login session key
1892 @param holdid The id of the hold
1898 my( $self, $conn, $auth, $holdid ) = @_;
1900 my ($hold, $evt) = $U->fetch_hold($holdid);
1901 return $evt if $evt;
1902 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1903 return $evt if $evt;
1904 $evt = _reset_hold($self, $reqr, $hold);
1905 return $evt if $evt;
1910 __PACKAGE__->register_method(
1911 method => 'reset_hold_batch',
1912 api_name => 'open-ils.circ.hold.reset.batch'
1915 sub reset_hold_batch {
1916 my($self, $conn, $auth, $hold_ids) = @_;
1918 my $e = new_editor(authtoken => $auth);
1919 return $e->event unless $e->checkauth;
1921 for my $hold_id ($hold_ids) {
1923 my $hold = $e->retrieve_action_hold_request(
1924 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
1925 or return $e->event;
1927 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1928 _reset_hold($self, $e->requestor, $hold);
1936 my ($self, $reqr, $hold) = @_;
1938 my $e = new_editor(xact =>1, requestor => $reqr);
1940 $logger->info("reseting hold ".$hold->id);
1942 my $hid = $hold->id;
1944 if( $hold->capture_time and $hold->current_copy ) {
1946 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1947 or return $e->die_event;
1949 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1950 $logger->info("setting copy to status 'reshelving' on hold retarget");
1951 $copy->status(OILS_COPY_STATUS_RESHELVING);
1952 $copy->editor($e->requestor->id);
1953 $copy->edit_date('now');
1954 $e->update_asset_copy($copy) or return $e->die_event;
1956 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1958 # We don't want the copy to remain "in transit"
1959 $copy->status(OILS_COPY_STATUS_RESHELVING);
1960 $logger->warn("! reseting hold [$hid] that is in transit");
1961 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
1964 my $trans = $e->retrieve_action_transit_copy($transid);
1966 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1967 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
1968 $logger->info("Transit abort completed with result $evt");
1969 unless ("$evt" eq 1) {
1978 $hold->clear_capture_time;
1979 $hold->clear_current_copy;
1980 $hold->clear_shelf_time;
1981 $hold->clear_shelf_expire_time;
1982 $hold->clear_current_shelf_lib;
1984 $e->update_action_hold_request($hold) or return $e->die_event;
1988 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1994 __PACKAGE__->register_method(
1995 method => 'fetch_open_title_holds',
1996 api_name => 'open-ils.circ.open_holds.retrieve',
1998 Returns a list ids of un-fulfilled holds for a given title id
1999 @param authtoken The login session key
2000 @param id the id of the item whose holds we want to retrieve
2001 @param type The hold type - M, T, I, V, C, F, R
2005 sub fetch_open_title_holds {
2006 my( $self, $conn, $auth, $id, $type, $org ) = @_;
2007 my $e = new_editor( authtoken => $auth );
2008 return $e->event unless $e->checkauth;
2011 $org ||= $e->requestor->ws_ou;
2013 # return $e->search_action_hold_request(
2014 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
2016 # XXX make me return IDs in the future ^--
2017 my $holds = $e->search_action_hold_request(
2020 cancel_time => undef,
2022 fulfillment_time => undef
2026 flesh_hold_transits($holds);
2031 sub flesh_hold_transits {
2033 for my $hold ( @$holds ) {
2035 $apputils->simplereq(
2037 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
2038 { hold => $hold->id },
2039 { order_by => { ahtc => 'id desc' }, limit => 1 }
2045 sub flesh_hold_notices {
2046 my( $holds, $e ) = @_;
2047 $e ||= new_editor();
2049 for my $hold (@$holds) {
2050 my $notices = $e->search_action_hold_notification(
2052 { hold => $hold->id },
2053 { order_by => { anh => 'notify_time desc' } },
2058 $hold->notify_count(scalar(@$notices));
2060 my $n = $e->retrieve_action_hold_notification($$notices[0])
2061 or return $e->event;
2062 $hold->notify_time($n->notify_time);
2068 __PACKAGE__->register_method(
2069 method => 'fetch_captured_holds',
2070 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
2074 Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
2075 @param authtoken The login session key
2076 @param org The org id of the location in question
2077 @param match_copy A specific copy to limit to
2081 __PACKAGE__->register_method(
2082 method => 'fetch_captured_holds',
2083 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
2087 Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
2088 @param authtoken The login session key
2089 @param org The org id of the location in question
2090 @param match_copy A specific copy to limit to
2094 __PACKAGE__->register_method(
2095 method => 'fetch_captured_holds',
2096 api_name => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
2100 Returns list ids of shelf-expired un-fulfilled holds for a given title id
2101 @param authtoken The login session key
2102 @param org The org id of the location in question
2103 @param match_copy A specific copy to limit to
2107 __PACKAGE__->register_method(
2108 method => 'fetch_captured_holds',
2110 'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
2114 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2115 for a given shelf lib
2119 __PACKAGE__->register_method(
2120 method => 'fetch_captured_holds',
2122 'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
2126 Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
2127 for a given shelf lib
2132 sub fetch_captured_holds {
2133 my( $self, $conn, $auth, $org, $match_copy ) = @_;
2135 my $e = new_editor(authtoken => $auth);
2136 return $e->die_event unless $e->checkauth;
2137 return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
2139 $org ||= $e->requestor->ws_ou;
2141 my $current_copy = { '!=' => undef };
2142 $current_copy = { '=' => $match_copy } if $match_copy;
2145 select => { alhr => ['id'] },
2150 fkey => 'current_copy'
2155 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
2157 capture_time => { "!=" => undef },
2158 current_copy => $current_copy,
2159 fulfillment_time => undef,
2160 current_shelf_lib => $org
2164 if($self->api_name =~ /expired/) {
2165 $query->{'where'}->{'+alhr'}->{'-or'} = {
2166 shelf_expire_time => { '<' => 'today'},
2167 cancel_time => { '!=' => undef },
2170 my $hold_ids = $e->json_query( $query );
2172 if ($self->api_name =~ /wrong_shelf/) {
2173 # fetch holds whose current_shelf_lib is $org, but whose pickup
2174 # lib is some other org unit. Ignore already-retrieved holds.
2176 pickup_lib_changed_on_shelf_holds(
2177 $e, $org, [map {$_->{id}} @$hold_ids]);
2178 # match the layout of other items in $hold_ids
2179 push (@$hold_ids, {id => $_}) for @$wrong_shelf;
2183 for my $hold_id (@$hold_ids) {
2184 if($self->api_name =~ /id_list/) {
2185 $conn->respond($hold_id->{id});
2189 $e->retrieve_action_hold_request([
2193 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
2194 order_by => {anh => 'notify_time desc'}
2204 __PACKAGE__->register_method(
2205 method => "print_expired_holds_stream",
2206 api_name => "open-ils.circ.captured_holds.expired.print.stream",
2210 sub print_expired_holds_stream {
2211 my ($self, $client, $auth, $params) = @_;
2213 # No need to check specific permissions: we're going to call another method
2214 # that will do that.
2215 my $e = new_editor("authtoken" => $auth);
2216 return $e->die_event unless $e->checkauth;
2218 delete($$params{org_id}) unless (int($$params{org_id}));
2219 delete($$params{limit}) unless (int($$params{limit}));
2220 delete($$params{offset}) unless (int($$params{offset}));
2221 delete($$params{chunk_size}) unless (int($$params{chunk_size}));
2222 delete($$params{chunk_size}) if ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
2223 $$params{chunk_size} ||= 10;
2225 $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
2227 my @hold_ids = $self->method_lookup(
2228 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
2229 )->run($auth, $params->{"org_id"});
2234 } elsif (defined $U->event_code($hold_ids[0])) {
2236 return $hold_ids[0];
2239 $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
2242 my @hid_chunk = splice @hold_ids, 0, $params->{"chunk_size"};
2244 my $result_chunk = $e->json_query({
2246 "acp" => ["barcode"],
2248 first_given_name second_given_name family_name alias
2257 "field" => "id", "fkey" => "current_copy",
2260 "field" => "id", "fkey" => "call_number",
2263 "field" => "id", "fkey" => "record"
2267 "acpl" => {"field" => "id", "fkey" => "location"}
2270 "au" => {"field" => "id", "fkey" => "usr"}
2273 "where" => {"+ahr" => {"id" => \@hid_chunk}}
2274 }) or return $e->die_event;
2275 $client->respond($result_chunk);
2282 __PACKAGE__->register_method(
2283 method => "check_title_hold_batch",
2284 api_name => "open-ils.circ.title_hold.is_possible.batch",
2287 desc => '@see open-ils.circ.title_hold.is_possible.batch',
2289 { desc => 'Authentication token', type => 'string'},
2290 { desc => 'Array of Hash of named parameters', type => 'array'},
2293 desc => 'Array of response objects',
2299 sub check_title_hold_batch {
2300 my($self, $client, $authtoken, $param_list, $oargs) = @_;
2301 foreach (@$param_list) {
2302 my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
2303 $client->respond($res);
2309 __PACKAGE__->register_method(
2310 method => "check_title_hold",
2311 api_name => "open-ils.circ.title_hold.is_possible",
2313 desc => 'Determines if a hold were to be placed by a given user, ' .
2314 'whether or not said hold would have any potential copies to fulfill it.' .
2315 'The named paramaters of the second argument include: ' .
2316 'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
2317 'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
2319 { desc => 'Authentication token', type => 'string'},
2320 { desc => 'Hash of named parameters', type => 'object'},
2323 desc => 'List of new message IDs (empty if none)',
2329 =head3 check_title_hold (token, hash)
2331 The named fields in the hash are:
2333 patronid - ID of the hold recipient (required)
2334 depth - hold range depth (default 0)
2335 pickup_lib - destination for hold, fallback value for selection_ou
2336 selection_ou - ID of org_unit establishing hard and soft hold boundary settings
2337 issuanceid - ID of the issuance to be held, required for Issuance level hold
2338 partid - ID of the monograph part to be held, required for monograph part level hold
2339 titleid - ID (BRN) of the title to be held, required for Title level hold
2340 volume_id - required for Volume level hold
2341 copy_id - required for Copy level hold
2342 mrid - required for Meta-record level hold
2343 hold_type - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record (default "T")
2345 All key/value pairs are passed on to do_possibility_checks.
2349 # FIXME: better params checking. what other params are required, if any?
2350 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
2351 # FIXME: for example, $depth gets a default value, but then $$params{depth} is still
2352 # used in conditionals, where it may be undefined, causing a warning.
2353 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
2355 sub check_title_hold {
2356 my( $self, $client, $authtoken, $params ) = @_;
2357 my $e = new_editor(authtoken=>$authtoken);
2358 return $e->event unless $e->checkauth;
2360 my %params = %$params;
2361 my $depth = $params{depth} || 0;
2362 my $selection_ou = $params{selection_ou} || $params{pickup_lib};
2363 my $oargs = $params{oargs} || {};
2365 if($oargs->{events}) {
2366 @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
2370 my $patron = $e->retrieve_actor_user($params{patronid})
2371 or return $e->event;
2373 if( $e->requestor->id ne $patron->id ) {
2374 return $e->event unless
2375 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
2378 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
2380 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
2381 or return $e->event;
2383 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
2384 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
2387 my $return_depth = $hard_boundary; # default depth to return on success
2388 if(defined $soft_boundary and $depth < $soft_boundary) {
2389 # work up the tree and as soon as we find a potential copy, use that depth
2390 # also, make sure we don't go past the hard boundary if it exists
2392 # our min boundary is the greater of user-specified boundary or hard boundary
2393 my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
2394 $hard_boundary : $depth;
2396 my $depth = $soft_boundary;
2397 while($depth >= $min_depth) {
2398 $logger->info("performing hold possibility check with soft boundary $depth");
2399 @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
2401 $return_depth = $depth;
2406 } elsif(defined $hard_boundary and $depth < $hard_boundary) {
2407 # there is no soft boundary, enforce the hard boundary if it exists
2408 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
2409 @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
2411 # no boundaries defined, fall back to user specifed boundary or no boundary
2412 $logger->info("performing hold possibility check with no boundary");
2413 @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
2416 my $place_unfillable = 0;
2417 $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
2422 "depth" => $return_depth,
2423 "local_avail" => $status[1]
2425 } elsif ($status[2]) {
2426 my $n = scalar @{$status[2]};
2427 return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2429 return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
2435 sub do_possibility_checks {
2436 my($e, $patron, $request_lib, $depth, %params) = @_;
2438 my $issuanceid = $params{issuanceid} || "";
2439 my $partid = $params{partid} || "";
2440 my $titleid = $params{titleid} || "";
2441 my $volid = $params{volume_id};
2442 my $copyid = $params{copy_id};
2443 my $mrid = $params{mrid} || "";
2444 my $pickup_lib = $params{pickup_lib};
2445 my $hold_type = $params{hold_type} || 'T';
2446 my $selection_ou = $params{selection_ou} || $pickup_lib;
2447 my $holdable_formats = $params{holdable_formats};
2448 my $oargs = $params{oargs} || {};
2455 if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
2457 return $e->event unless $copy = $e->retrieve_asset_copy($copyid);
2458 return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
2459 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2461 return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
2462 return verify_copy_for_hold(
2463 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
2466 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
2468 return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
2469 return $e->event unless $title = $e->retrieve_biblio_record_entry($volume->record);
2471 return _check_volume_hold_is_possible(
2472 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2475 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
2477 return _check_title_hold_is_possible(
2478 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
2481 } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
2483 return _check_issuance_hold_is_possible(
2484 $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2487 } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
2489 return _check_monopart_hold_is_possible(
2490 $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
2493 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
2495 # pasing undef as the depth to filtered_records causes the depth
2496 # of the selection_ou to be used, which is not what we want here.
2499 my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
2501 for my $rec (@$recs) {
2502 @status = _check_title_hold_is_possible(
2503 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
2509 # else { Unrecognized hold_type ! } # FIXME: return error? or 0?
2512 sub MR_filter_records {
2519 my $opac_visible = shift;
2521 my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
2522 return $U->storagereq(
2523 'open-ils.storage.metarecord.filtered_records.atomic',
2524 $m, $f, $org_at_depth, $opac_visible
2527 __PACKAGE__->register_method(
2528 method => 'MR_filter_records',
2529 api_name => 'open-ils.circ.holds.metarecord.filtered_records',
2534 sub create_ranged_org_filter {
2535 my($e, $selection_ou, $depth) = @_;
2537 # find the orgs from which this hold may be fulfilled,
2538 # based on the selection_ou and depth
2540 my $top_org = $e->search_actor_org_unit([
2541 {parent_ou => undef},
2542 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
2545 return () if $depth == $top_org->ou_type->depth;
2547 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
2548 %org_filter = (circ_lib => []);
2549 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
2551 $logger->info("hold org filter at depth $depth and selection_ou ".
2552 "$selection_ou created list of @{$org_filter{circ_lib}}");
2558 sub _check_title_hold_is_possible {
2559 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
2560 # $holdable_formats is now unused. We pre-filter the MR's records.
2562 my $e = new_editor();
2563 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2565 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2566 my $copies = $e->json_query(
2568 select => { acp => ['id', 'circ_lib'] },
2573 fkey => 'call_number',
2574 filter => { record => $titleid }
2576 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
2577 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' },
2578 acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
2582 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
2583 '+acpm' => { target_copy => undef } # ignore part-linked copies
2588 $logger->info("title possible found ".scalar(@$copies)." potential copies");
2592 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2593 "payload" => {"fail_part" => "no_ultimate_items"}
2598 # -----------------------------------------------------------------------
2599 # sort the copies into buckets based on their circ_lib proximity to
2600 # the patron's home_ou.
2601 # -----------------------------------------------------------------------
2603 my $home_org = $patron->home_ou;
2604 my $req_org = $request_lib->id;
2606 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2608 $prox_cache{$home_org} =
2609 $e->search_actor_org_unit_proximity({from_org => $home_org})
2610 unless $prox_cache{$home_org};
2611 my $home_prox = $prox_cache{$home_org};
2614 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2615 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2617 my @keys = sort { $a <=> $b } keys %buckets;
2620 if( $home_org ne $req_org ) {
2621 # -----------------------------------------------------------------------
2622 # shove the copies close to the request_lib into the primary buckets
2623 # directly before the farthest away copies. That way, they are not
2624 # given priority, but they are checked before the farthest copies.
2625 # -----------------------------------------------------------------------
2626 $prox_cache{$req_org} =
2627 $e->search_actor_org_unit_proximity({from_org => $req_org})
2628 unless $prox_cache{$req_org};
2629 my $req_prox = $prox_cache{$req_org};
2632 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2633 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2635 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2636 my $new_key = $highest_key - 0.5; # right before the farthest prox
2637 my @keys2 = sort { $a <=> $b } keys %buckets2;
2638 for my $key (@keys2) {
2639 last if $key >= $highest_key;
2640 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2644 @keys = sort { $a <=> $b } keys %buckets;
2649 my $age_protect_only = 0;
2650 OUTER: for my $key (@keys) {
2651 my @cps = @{$buckets{$key}};
2653 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2655 for my $copyid (@cps) {
2657 next if $seen{$copyid};
2658 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2659 my $copy = $e->retrieve_asset_copy($copyid);
2660 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2662 unless($title) { # grab the title if we don't already have it
2663 my $vol = $e->retrieve_asset_call_number(
2664 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2665 $title = $vol->record;
2668 @status = verify_copy_for_hold(
2669 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2671 $age_protect_only ||= $status[3];
2672 last OUTER if $status[0];
2676 $status[3] = $age_protect_only;
2680 sub _check_issuance_hold_is_possible {
2681 my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2683 my $e = new_editor();
2684 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2686 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2687 my $copies = $e->json_query(
2689 select => { acp => ['id', 'circ_lib'] },
2695 filter => { issuance => $issuanceid }
2697 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
2698 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
2702 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
2708 $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
2712 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
2713 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2718 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2719 "payload" => {"fail_part" => "no_ultimate_items"}
2727 # -----------------------------------------------------------------------
2728 # sort the copies into buckets based on their circ_lib proximity to
2729 # the patron's home_ou.
2730 # -----------------------------------------------------------------------
2732 my $home_org = $patron->home_ou;
2733 my $req_org = $request_lib->id;
2735 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2737 $prox_cache{$home_org} =
2738 $e->search_actor_org_unit_proximity({from_org => $home_org})
2739 unless $prox_cache{$home_org};
2740 my $home_prox = $prox_cache{$home_org};
2743 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2744 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2746 my @keys = sort { $a <=> $b } keys %buckets;
2749 if( $home_org ne $req_org ) {
2750 # -----------------------------------------------------------------------
2751 # shove the copies close to the request_lib into the primary buckets
2752 # directly before the farthest away copies. That way, they are not
2753 # given priority, but they are checked before the farthest copies.
2754 # -----------------------------------------------------------------------
2755 $prox_cache{$req_org} =
2756 $e->search_actor_org_unit_proximity({from_org => $req_org})
2757 unless $prox_cache{$req_org};
2758 my $req_prox = $prox_cache{$req_org};
2761 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2762 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2764 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2765 my $new_key = $highest_key - 0.5; # right before the farthest prox
2766 my @keys2 = sort { $a <=> $b } keys %buckets2;
2767 for my $key (@keys2) {
2768 last if $key >= $highest_key;
2769 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2773 @keys = sort { $a <=> $b } keys %buckets;
2778 my $age_protect_only = 0;
2779 OUTER: for my $key (@keys) {
2780 my @cps = @{$buckets{$key}};
2782 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2784 for my $copyid (@cps) {
2786 next if $seen{$copyid};
2787 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2788 my $copy = $e->retrieve_asset_copy($copyid);
2789 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2791 unless($title) { # grab the title if we don't already have it
2792 my $vol = $e->retrieve_asset_call_number(
2793 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2794 $title = $vol->record;
2797 @status = verify_copy_for_hold(
2798 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2800 $age_protect_only ||= $status[3];
2801 last OUTER if $status[0];
2806 if (!defined($empty_ok)) {
2807 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
2808 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2811 return (1,0) if ($empty_ok);
2813 $status[3] = $age_protect_only;
2817 sub _check_monopart_hold_is_possible {
2818 my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2820 my $e = new_editor();
2821 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
2823 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
2824 my $copies = $e->json_query(
2826 select => { acp => ['id', 'circ_lib'] },
2830 field => 'target_copy',
2832 filter => { part => $partid }
2834 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
2835 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
2839 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
2845 $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
2849 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
2850 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2855 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2856 "payload" => {"fail_part" => "no_ultimate_items"}
2864 # -----------------------------------------------------------------------
2865 # sort the copies into buckets based on their circ_lib proximity to
2866 # the patron's home_ou.
2867 # -----------------------------------------------------------------------
2869 my $home_org = $patron->home_ou;
2870 my $req_org = $request_lib->id;
2872 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
2874 $prox_cache{$home_org} =
2875 $e->search_actor_org_unit_proximity({from_org => $home_org})
2876 unless $prox_cache{$home_org};
2877 my $home_prox = $prox_cache{$home_org};
2880 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
2881 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2883 my @keys = sort { $a <=> $b } keys %buckets;
2886 if( $home_org ne $req_org ) {
2887 # -----------------------------------------------------------------------
2888 # shove the copies close to the request_lib into the primary buckets
2889 # directly before the farthest away copies. That way, they are not
2890 # given priority, but they are checked before the farthest copies.
2891 # -----------------------------------------------------------------------
2892 $prox_cache{$req_org} =
2893 $e->search_actor_org_unit_proximity({from_org => $req_org})
2894 unless $prox_cache{$req_org};
2895 my $req_prox = $prox_cache{$req_org};
2898 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
2899 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
2901 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
2902 my $new_key = $highest_key - 0.5; # right before the farthest prox
2903 my @keys2 = sort { $a <=> $b } keys %buckets2;
2904 for my $key (@keys2) {
2905 last if $key >= $highest_key;
2906 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
2910 @keys = sort { $a <=> $b } keys %buckets;
2915 my $age_protect_only = 0;
2916 OUTER: for my $key (@keys) {
2917 my @cps = @{$buckets{$key}};
2919 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
2921 for my $copyid (@cps) {
2923 next if $seen{$copyid};
2924 $seen{$copyid} = 1; # there could be dupes given the merged buckets
2925 my $copy = $e->retrieve_asset_copy($copyid);
2926 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
2928 unless($title) { # grab the title if we don't already have it
2929 my $vol = $e->retrieve_asset_call_number(
2930 [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
2931 $title = $vol->record;
2934 @status = verify_copy_for_hold(
2935 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
2937 $age_protect_only ||= $status[3];
2938 last OUTER if $status[0];
2943 if (!defined($empty_ok)) {
2944 $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
2945 $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
2948 return (1,0) if ($empty_ok);
2950 $status[3] = $age_protect_only;
2955 sub _check_volume_hold_is_possible {
2956 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
2957 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
2958 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
2959 $logger->info("checking possibility of volume hold for volume ".$vol->id);
2961 my $filter_copies = [];
2962 for my $copy (@$copies) {
2963 # ignore part-mapped copies for regular volume level holds
2964 push(@$filter_copies, $copy) unless
2965 new_editor->search_asset_copy_part_map({target_copy => $copy->id})->[0];
2967 $copies = $filter_copies;
2972 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
2973 "payload" => {"fail_part" => "no_ultimate_items"}
2979 my $age_protect_only = 0;
2980 for my $copy ( @$copies ) {
2981 @status = verify_copy_for_hold(
2982 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
2983 $age_protect_only ||= $status[3];
2986 $status[3] = $age_protect_only;
2992 sub verify_copy_for_hold {
2993 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
2994 # $oargs should be undef unless we're overriding.
2995 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
2996 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
2999 requestor => $requestor,
3002 title_descriptor => $title->fixed_fields,
3003 pickup_lib => $pickup_lib,
3004 request_lib => $request_lib,
3006 show_event_list => 1
3010 # Check for override permissions on events.
3011 if ($oargs && $permitted && scalar @$permitted) {
3012 # Remove the events from permitted that we can override.
3013 if ($oargs->{events}) {
3014 foreach my $evt (@{$oargs->{events}}) {
3015 $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
3018 # Now, we handle the override all case by checking remaining
3019 # events against override permissions.
3020 if (scalar @$permitted && $oargs->{all}) {
3021 # Pre-set events and failed members of oargs to empty
3022 # arrays, if they are not set, yet.
3023 $oargs->{events} = [] unless ($oargs->{events});
3024 $oargs->{failed} = [] unless ($oargs->{failed});
3025 # When we're done with these checks, we swap permitted
3026 # with a reference to @disallowed.
3027 my @disallowed = ();
3028 foreach my $evt (@{$permitted}) {
3029 # Check if we've already seen the event in this
3030 # session and it failed.
3031 if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
3032 push(@disallowed, $evt);
3034 # We have to check if the requestor has the
3035 # override permission.
3037 # AppUtils::check_user_perms returns the perm if
3038 # the user doesn't have it, undef if they do.
3039 if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
3040 push(@disallowed, $evt);
3041 push(@{$oargs->{failed}}, $evt->{textcode});
3043 push(@{$oargs->{events}}, $evt->{textcode});
3047 $permitted = \@disallowed;
3051 my $age_protect_only = 0;
3052 if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
3053 $age_protect_only = 1;
3057 (not scalar @$permitted), # true if permitted is an empty arrayref
3058 ( # XXX This test is of very dubious value; someone should figure
3059 # out what if anything is checking this value
3060 ($copy->circ_lib == $pickup_lib) and
3061 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
3070 sub find_nearest_permitted_hold {
3073 my $editor = shift; # CStoreEditor object
3074 my $copy = shift; # copy to target
3075 my $user = shift; # staff
3076 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
3078 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
3080 my $bc = $copy->barcode;
3082 # find any existing holds that already target this copy
3083 my $old_holds = $editor->search_action_hold_request(
3084 { current_copy => $copy->id,
3085 cancel_time => undef,
3086 capture_time => undef
3090 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
3092 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
3093 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
3095 my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
3097 # the nearest_hold API call now needs this
3098 $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
3099 unless ref $copy->call_number;
3101 # search for what should be the best holds for this copy to fulfill
3102 my $best_holds = $U->storagereq(
3103 "open-ils.storage.action.hold_request.nearest_hold.atomic",
3104 $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
3106 # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
3108 for my $holdid (@$old_holds) {
3109 next unless $holdid;
3110 push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
3114 unless(@$best_holds) {
3115 $logger->info("circulator: no suitable holds found for copy $bc");
3116 return (undef, $evt);
3122 # for each potential hold, we have to run the permit script
3123 # to make sure the hold is actually permitted.
3126 for my $holdid (@$best_holds) {
3127 next unless $holdid;
3128 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
3130 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
3131 # Force and recall holds bypass all rules
3132 if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
3136 my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
3137 my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
3139 $reqr_cache{$hold->requestor} = $reqr;
3140 $org_cache{$hold->request_lib} = $rlib;
3142 # see if this hold is permitted
3143 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
3145 patron_id => $hold->usr,
3148 pickup_lib => $hold->pickup_lib,
3149 request_lib => $rlib,
3161 unless( $best_hold ) { # no "good" permitted holds were found
3163 $logger->info("circulator: no suitable holds found for copy $bc");
3164 return (undef, $evt);
3167 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
3169 # indicate a permitted hold was found
3170 return $best_hold if $check_only;
3172 # we've found a permitted hold. we need to "grab" the copy
3173 # to prevent re-targeted holds (next part) from re-grabbing the copy
3174 $best_hold->current_copy($copy->id);
3175 $editor->update_action_hold_request($best_hold)
3176 or return (undef, $editor->event);
3181 # re-target any other holds that already target this copy
3182 for my $old_hold (@$old_holds) {
3183 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
3184 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
3185 $old_hold->id." after a better hold [".$best_hold->id."] was found");
3186 $old_hold->clear_current_copy;
3187 $old_hold->clear_prev_check_time;
3188 $editor->update_action_hold_request($old_hold)
3189 or return (undef, $editor->event);
3190 push(@retarget, $old_hold->id);
3193 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
3201 __PACKAGE__->register_method(
3202 method => 'all_rec_holds',
3203 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
3207 my( $self, $conn, $auth, $title_id, $args ) = @_;
3209 my $e = new_editor(authtoken=>$auth);
3210 $e->checkauth or return $e->event;
3211 $e->allowed('VIEW_HOLD') or return $e->event;
3214 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
3215 $args->{cancel_time} = undef;
3218 metarecord_holds => []
3220 , volume_holds => []
3222 , recall_holds => []
3225 , issuance_holds => []
3228 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
3230 $resp->{metarecord_holds} = $e->search_action_hold_request(
3231 { hold_type => OILS_HOLD_TYPE_METARECORD,
3232 target => $mr_map->metarecord,
3238 $resp->{title_holds} = $e->search_action_hold_request(
3240 hold_type => OILS_HOLD_TYPE_TITLE,
3241 target => $title_id,
3245 my $parts = $e->search_biblio_monograph_part(
3251 $resp->{part_holds} = $e->search_action_hold_request(
3253 hold_type => OILS_HOLD_TYPE_MONOPART,
3259 my $subs = $e->search_serial_subscription(
3260 { record_entry => $title_id }, {idlist=>1});
3263 my $issuances = $e->search_serial_issuance(
3264 {subscription => $subs}, {idlist=>1}
3268 $resp->{issuance_holds} = $e->search_action_hold_request(
3270 hold_type => OILS_HOLD_TYPE_ISSUANCE,
3271 target => $issuances,
3278 my $vols = $e->search_asset_call_number(
3279 { record => $title_id, deleted => 'f' }, {idlist=>1});
3281 return $resp unless @$vols;
3283 $resp->{volume_holds} = $e->search_action_hold_request(
3285 hold_type => OILS_HOLD_TYPE_VOLUME,
3290 my $copies = $e->search_asset_copy(
3291 { call_number => $vols, deleted => 'f' }, {idlist=>1});
3293 return $resp unless @$copies;
3295 $resp->{copy_holds} = $e->search_action_hold_request(
3297 hold_type => OILS_HOLD_TYPE_COPY,
3302 $resp->{recall_holds} = $e->search_action_hold_request(
3304 hold_type => OILS_HOLD_TYPE_RECALL,
3309 $resp->{force_holds} = $e->search_action_hold_request(
3311 hold_type => OILS_HOLD_TYPE_FORCE,
3323 __PACKAGE__->register_method(
3324 method => 'uber_hold',
3326 api_name => 'open-ils.circ.hold.details.retrieve'
3330 my($self, $client, $auth, $hold_id, $args) = @_;
3331 my $e = new_editor(authtoken=>$auth);
3332 $e->checkauth or return $e->event;
3333 return uber_hold_impl($e, $hold_id, $args);
3336 __PACKAGE__->register_method(
3337 method => 'batch_uber_hold',
3340 api_name => 'open-ils.circ.hold.details.batch.retrieve'
3343 sub batch_uber_hold {
3344 my($self, $client, $auth, $hold_ids, $args) = @_;
3345 my $e = new_editor(authtoken=>$auth);
3346 $e->checkauth or return $e->event;
3347 $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
3351 sub uber_hold_impl {
3352 my($e, $hold_id, $args) = @_;
3355 my $hold = $e->retrieve_action_hold_request(
3360 flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
3363 ) or return $e->event;
3365 if($hold->usr->id ne $e->requestor->id) {
3366 # caller is asking for someone else's hold
3367 $e->allowed('VIEW_HOLD') or return $e->event;
3368 $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
3369 [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3372 # caller is asking for own hold, but may not have permission to view staff notes
3373 unless($e->allowed('VIEW_HOLD')) {
3374 $hold->notes( # filter out any staff notes (unless marked as public)
3375 [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
3379 my $user = $hold->usr;
3380 $hold->usr($user->id);
3383 my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args->{suppress_mvr});
3385 flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
3386 flesh_hold_transits([$hold]) unless $args->{suppress_transits};
3388 my $details = retrieve_hold_queue_status_impl($e, $hold);
3393 ($copy ? (copy => $copy) : ()),
3394 ($volume ? (volume => $volume) : ()),
3395 ($issuance ? (issuance => $issuance) : ()),
3396 ($part ? (part => $part) : ()),
3397 ($args->{include_bre} ? (bre => $bre) : ()),
3398 ($args->{suppress_mvr} ? () : (mvr => $mvr)),
3402 $resp->{copy}->location(
3403 $e->retrieve_asset_copy_location($resp->{copy}->location))
3404 if $resp->{copy} and $args->{flesh_acpl};
3406 unless($args->{suppress_patron_details}) {
3407 my $card = $e->retrieve_actor_card($user->card) or return $e->event;
3408 $resp->{patron_first} = $user->first_given_name,
3409 $resp->{patron_last} = $user->family_name,
3410 $resp->{patron_barcode} = $card->barcode,
3411 $resp->{patron_alias} = $user->alias,
3419 # -----------------------------------------------------
3420 # Returns the MVR object that represents what the
3422 # -----------------------------------------------------
3424 my( $e, $hold, $no_mvr ) = @_;
3432 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
3433 my $mr = $e->retrieve_metabib_metarecord($hold->target)
3434 or return $e->event;
3435 $tid = $mr->master_record;
3437 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
3438 $tid = $hold->target;
3440 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
3441 $volume = $e->retrieve_asset_call_number($hold->target)
3442 or return $e->event;
3443 $tid = $volume->record;
3445 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
3446 $issuance = $e->retrieve_serial_issuance([
3448 {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
3449 ]) or return $e->event;
3451 $tid = $issuance->subscription->record_entry;
3453 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
3454 $part = $e->retrieve_biblio_monograph_part([
3456 ]) or return $e->event;
3458 $tid = $part->record;
3460 } 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 ) {
3461 $copy = $e->retrieve_asset_copy([
3463 {flesh => 1, flesh_fields => {acp => ['call_number']}}
3464 ]) or return $e->event;
3466 $volume = $copy->call_number;
3467 $tid = $volume->record;
3470 if(!$copy and ref $hold->current_copy ) {
3471 $copy = $hold->current_copy;
3472 $hold->current_copy($copy->id);
3475 if(!$volume and $copy) {
3476 $volume = $e->retrieve_asset_call_number($copy->call_number);
3479 # TODO return metarcord mvr for M holds
3480 my $title = $e->retrieve_biblio_record_entry($tid);
3481 return ( ($no_mvr) ? undef : $U->record_to_mvr($title), $volume, $copy, $issuance, $part, $title );
3484 __PACKAGE__->register_method(
3485 method => 'clear_shelf_cache',
3486 api_name => 'open-ils.circ.hold.clear_shelf.get_cache',
3490 Returns the holds processed with the given cache key
3495 sub clear_shelf_cache {
3496 my($self, $client, $auth, $cache_key, $chunk_size) = @_;
3497 my $e = new_editor(authtoken => $auth, xact => 1);
3498 return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
3501 my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
3504 $logger->info("no hold data found in cache"); # XXX TODO return event
3510 foreach (keys %$hold_data) {
3511 $maximum += scalar(@{ $hold_data->{$_} });
3513 $client->respond({"maximum" => $maximum, "progress" => 0});
3515 for my $action (sort keys %$hold_data) {
3516 while (@{$hold_data->{$action}}) {
3517 my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
3519 my $result_chunk = $e->json_query({
3521 "acp" => ["barcode"],
3523 first_given_name second_given_name family_name alias
3526 "acnp" => [{column => "label", alias => "prefix"}],
3527 "acns" => [{column => "label", alias => "suffix"}],
3535 "field" => "id", "fkey" => "current_copy",
3538 "field" => "id", "fkey" => "call_number",
3541 "field" => "id", "fkey" => "record"
3544 "field" => "id", "fkey" => "prefix"
3547 "field" => "id", "fkey" => "suffix"
3551 "acpl" => {"field" => "id", "fkey" => "location"}
3554 "au" => {"field" => "id", "fkey" => "usr"}
3557 "where" => {"+ahr" => {"id" => \@hid_chunk}}
3558 }, {"substream" => 1}) or return $e->die_event;
3562 +{"action" => $action, "hold_details" => $_}
3573 __PACKAGE__->register_method(
3574 method => 'clear_shelf_process',
3576 api_name => 'open-ils.circ.hold.clear_shelf.process',
3579 1. Find all holds that have expired on the holds shelf
3581 3. If a clear-shelf status is configured, put targeted copies into this status
3582 4. Divide copies into 3 groups: items to transit, items to reshelve, and items
3583 that are needed for holds. No subsequent action is taken on the holds
3584 or items after grouping.
3589 sub clear_shelf_process {
3590 my($self, $client, $auth, $org_id, $match_copy) = @_;
3592 my $e = new_editor(authtoken=>$auth);
3593 $e->checkauth or return $e->die_event;
3594 my $cache = OpenSRF::Utils::Cache->new('global');
3596 $org_id ||= $e->requestor->ws_ou;
3597 $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
3599 my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
3601 my @hold_ids = $self->method_lookup(
3602 "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
3603 )->run($auth, $org_id, $match_copy);
3608 my @canceled_holds; # newly canceled holds
3609 my $chunk_size = 25; # chunked status updates
3611 for my $hold_id (@hold_ids) {
3613 $logger->info("Clear shelf processing hold $hold_id");
3615 my $hold = $e->retrieve_action_hold_request([
3618 flesh_fields => {ahr => ['current_copy']}
3622 if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
3623 $hold->cancel_time('now');
3624 $hold->cancel_cause(2); # Hold Shelf expiration
3625 $e->update_action_hold_request($hold) or return $e->die_event;
3626 push(@canceled_holds, $hold_id);
3629 my $copy = $hold->current_copy;
3631 if($copy_status or $copy_status == 0) {
3632 # if a clear-shelf copy status is defined, update the copy
3633 $copy->status($copy_status);
3634 $copy->edit_date('now');
3635 $copy->editor($e->requestor->id);
3636 $e->update_asset_copy($copy) or return $e->die_event;
3639 push(@holds, $hold);
3640 $client->respond({maximum => scalar(@holds), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
3649 pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
3652 for my $hold (@holds) {
3654 my $copy = $hold->current_copy;
3655 my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
3657 if($alt_hold and !$match_copy) {
3659 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
3661 } elsif($copy->circ_lib != $e->requestor->ws_ou) {
3663 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
3667 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
3671 my $cache_key = md5_hex(time . $$ . rand());
3672 $logger->info("clear_shelf_cache: storing under $cache_key");
3673 $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours. configurable?
3675 # tell the client we're done
3676 $client->respond_complete({cache_key => $cache_key});
3679 # fire off the hold cancelation trigger and wait for response so don't flood the service
3681 # refetch the holds to pick up the caclulated cancel_time,
3682 # which may be needed by Action/Trigger
3684 my $updated_holds = [];
3685 $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
3688 $U->create_events_for_hook(
3689 'hold_request.cancel.expire_holds_shelf',
3690 $_, $org_id, undef, undef, 1) for @$updated_holds;
3693 # tell the client we're done
3694 $client->respond_complete;
3698 # returns IDs for holds that are on the holds shelf but
3699 # have had their pickup_libs change while on the shelf.
3700 sub pickup_lib_changed_on_shelf_holds {
3703 my $ignore_holds = shift;
3704 $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
3707 select => { alhr => ['id'] },
3712 fkey => 'current_copy'
3717 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
3719 capture_time => { "!=" => undef },
3720 fulfillment_time => undef,
3721 current_shelf_lib => $org_id,
3722 pickup_lib => {'!=' => {'+alhr' => 'current_shelf_lib'}}
3727 $query->{where}->{'+alhr'}->{id} =
3728 {'not in' => $ignore_holds} if @$ignore_holds;
3730 my $hold_ids = $e->json_query($query);
3731 return [ map { $_->{id} } @$hold_ids ];
3734 __PACKAGE__->register_method(
3735 method => 'usr_hold_summary',
3736 api_name => 'open-ils.circ.holds.user_summary',
3738 Returns a summary of holds statuses for a given user
3742 sub usr_hold_summary {
3743 my($self, $conn, $auth, $user_id) = @_;
3745 my $e = new_editor(authtoken=>$auth);
3746 $e->checkauth or return $e->event;
3747 $e->allowed('VIEW_HOLD') or return $e->event;
3749 my $holds = $e->search_action_hold_request(
3752 fulfillment_time => undef,
3753 cancel_time => undef,
3757 my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
3758 $summary{_hold_status($e, $_)} += 1 for @$holds;
3764 __PACKAGE__->register_method(
3765 method => 'hold_has_copy_at',
3766 api_name => 'open-ils.circ.hold.has_copy_at',
3769 'Returns the ID of the found copy and name of the shelving location if there is ' .
3770 'an available copy at the specified org unit. Returns empty hash otherwise. ' .
3771 'The anticipated use for this method is to determine whether an item is ' .
3772 'available at the library where the user is placing the hold (or, alternatively, '.
3773 'at the pickup library) to encourage bypassing the hold placement and just ' .
3774 'checking out the item.' ,
3776 { desc => 'Authentication Token', type => 'string' },
3777 { desc => 'Method Arguments. Options include: hold_type, hold_target, org_unit. '
3778 . 'hold_type is the hold type code (T, V, C, M, ...). '
3779 . 'hold_target is the identifier of the hold target object. '
3780 . 'org_unit is org unit ID.',
3785 desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
3791 sub hold_has_copy_at {
3792 my($self, $conn, $auth, $args) = @_;
3794 my $e = new_editor(authtoken=>$auth);
3795 $e->checkauth or return $e->event;
3797 my $hold_type = $$args{hold_type};
3798 my $hold_target = $$args{hold_target};
3799 my $org_unit = $$args{org_unit};
3802 select => {acp => ['id'], acpl => ['name']},
3805 acpl => {field => 'id', filter => { holdable => 't'}, fkey => 'location'},
3806 ccs => {field => 'id', filter => { holdable => 't'}, fkey => 'status' }
3809 where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit, status => [0,7]}},
3813 if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
3815 $query->{where}->{'+acp'}->{id} = $hold_target;
3817 } elsif($hold_type eq 'V') {
3819 $query->{where}->{'+acp'}->{call_number} = $hold_target;
3821 } elsif($hold_type eq 'P') {
3823 $query->{from}->{acp}->{acpm} = {
3824 field => 'target_copy',
3826 filter => {part => $hold_target},
3829 } elsif($hold_type eq 'I') {
3831 $query->{from}->{acp}->{sitem} = {
3834 filter => {issuance => $hold_target},
3837 } elsif($hold_type eq 'T') {
3839 $query->{from}->{acp}->{acn} = {
3841 fkey => 'call_number',
3845 filter => {id => $hold_target},
3853 $query->{from}->{acp}->{acn} = {
3855 fkey => 'call_number',
3864 filter => {metarecord => $hold_target},
3872 my $res = $e->json_query($query)->[0] or return {};
3873 return {copy => $res->{id}, location => $res->{name}} if $res;
3877 # returns true if the user already has an item checked out
3878 # that could be used to fulfill the requested hold.
3879 sub hold_item_is_checked_out {
3880 my($e, $user_id, $hold_type, $hold_target) = @_;
3883 select => {acp => ['id']},
3884 from => {acp => {}},
3888 in => { # copies for circs the user has checked out
3889 select => {circ => ['target_copy']},
3893 checkin_time => undef,
3895 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
3896 {stop_fines => undef}
3906 if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
3908 $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
3910 } elsif($hold_type eq 'V') {
3912 $query->{where}->{'+acp'}->{call_number} = $hold_target;
3914 } elsif($hold_type eq 'P') {
3916 $query->{from}->{acp}->{acpm} = {
3917 field => 'target_copy',
3919 filter => {part => $hold_target},
3922 } elsif($hold_type eq 'I') {
3924 $query->{from}->{acp}->{sitem} = {
3927 filter => {issuance => $hold_target},
3930 } elsif($hold_type eq 'T') {
3932 $query->{from}->{acp}->{acn} = {
3934 fkey => 'call_number',
3938 filter => {id => $hold_target},
3946 $query->{from}->{acp}->{acn} = {
3948 fkey => 'call_number',
3957 filter => {metarecord => $hold_target},
3965 return $e->json_query($query)->[0];
3968 __PACKAGE__->register_method(
3969 method => 'change_hold_title',
3970 api_name => 'open-ils.circ.hold.change_title',
3973 Updates all title level holds targeting the specified bibs to point a new bib./,
3975 { desc => 'Authentication Token', type => 'string' },
3976 { desc => 'New Target Bib Id', type => 'number' },
3977 { desc => 'Old Target Bib Ids', type => 'array' },
3979 return => { desc => '1 on success' }
3983 __PACKAGE__->register_method(
3984 method => 'change_hold_title_for_specific_holds',
3985 api_name => 'open-ils.circ.hold.change_title.specific_holds',
3988 Updates specified holds to target new bib./,
3990 { desc => 'Authentication Token', type => 'string' },
3991 { desc => 'New Target Bib Id', type => 'number' },
3992 { desc => 'Holds Ids for holds to update', type => 'array' },
3994 return => { desc => '1 on success' }
3999 sub change_hold_title {
4000 my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
4002 my $e = new_editor(authtoken=>$auth, xact=>1);
4003 return $e->die_event unless $e->checkauth;
4005 my $holds = $e->search_action_hold_request(
4008 cancel_time => undef,
4009 fulfillment_time => undef,
4015 flesh_fields => { ahr => ['usr'] }
4021 for my $hold (@$holds) {
4022 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4023 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4024 $hold->target( $new_bib_id );
4025 $e->update_action_hold_request($hold) or return $e->die_event;
4030 _reset_hold($self, $e->requestor, $_) for @$holds;
4035 sub change_hold_title_for_specific_holds {
4036 my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
4038 my $e = new_editor(authtoken=>$auth, xact=>1);
4039 return $e->die_event unless $e->checkauth;
4041 my $holds = $e->search_action_hold_request(
4044 cancel_time => undef,
4045 fulfillment_time => undef,
4051 flesh_fields => { ahr => ['usr'] }
4057 for my $hold (@$holds) {
4058 $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
4059 $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
4060 $hold->target( $new_bib_id );
4061 $e->update_action_hold_request($hold) or return $e->die_event;
4066 _reset_hold($self, $e->requestor, $_) for @$holds;
4071 __PACKAGE__->register_method(
4072 method => 'rec_hold_count',
4073 api_name => 'open-ils.circ.bre.holds.count',
4075 desc => q/Returns the total number of holds that target the
4076 selected bib record or its associated copies and call_numbers/,
4078 { desc => 'Bib ID', type => 'number' },
4079 { desc => q/Optional arguments. Supported arguments include:
4080 "pickup_lib_descendant" -> limit holds to those whose pickup
4081 library is equal to or is a child of the provided org unit/,
4085 return => {desc => 'Hold count', type => 'number'}
4089 __PACKAGE__->register_method(
4090 method => 'rec_hold_count',
4091 api_name => 'open-ils.circ.mmr.holds.count',
4093 desc => q/Returns the total number of holds that target the
4094 selected metarecord or its associated copies, call_numbers, and bib records/,
4096 { desc => 'Metarecord ID', type => 'number' },
4098 return => {desc => 'Hold count', type => 'number'}
4102 # XXX Need to add type I holds to these counts
4103 sub rec_hold_count {
4104 my($self, $conn, $target_id, $args) = @_;
4111 filter => {metarecord => $target_id}
4118 filter => { id => $target_id },
4123 if($self->api_name =~ /mmr/) {
4124 delete $bre_join->{bre}->{filter};
4125 $bre_join->{bre}->{join} = $mmr_join;
4131 fkey => 'call_number',
4137 select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
4141 cancel_time => undef,
4142 fulfillment_time => undef,
4146 hold_type => [qw/C F R/],
4149 select => {acp => ['id']},
4150 from => { acp => $cn_join }
4160 select => {acn => ['id']},
4161 from => {acn => $bre_join}
4171 select => {bmp => ['id']},
4172 from => {bmp => $bre_join}
4180 target => $target_id
4188 if($self->api_name =~ /mmr/) {
4189 $query->{where}->{'+ahr'}->{'-or'}->[3] = {
4194 select => {bre => ['id']},
4195 from => {bre => $mmr_join}
4201 $query->{where}->{'+ahr'}->{'-or'}->[4] = {
4204 target => $target_id
4210 if (my $pld = $args->{pickup_lib_descendant}) {
4212 my $top_ou = new_editor()->search_actor_org_unit(
4213 {parent_ou => undef}
4214 )->[0]; # XXX Assumes single root node. Not alone in this...
4216 $query->{where}->{'+ahr'}->{pickup_lib} = {
4218 select => {aou => [{
4220 transform => 'actor.org_unit_descendants',
4221 result_field => 'id'
4224 where => {id => $pld}
4226 } if ($pld != $top_ou->id);
4230 return new_editor()->json_query($query)->[0]->{count};
4233 # A helper function to calculate a hold's expiration time at a given
4234 # org_unit. Takes the org_unit as an argument and returns either the
4235 # hold expire time as an ISO8601 string or undef if there is no hold
4236 # expiration interval set for the subject ou.
4237 sub calculate_expire_time
4240 my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
4242 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
4243 return $U->epoch2ISO8601($date->epoch);
4249 __PACKAGE__->register_method(
4250 method => 'mr_hold_filter_attrs',
4251 api_name => 'open-ils.circ.mmr.holds.filters',
4256 Returns the set of available formats and languages for the
4257 constituent records of the provided metarcord.
4258 If an array of hold IDs is also provided, information about
4259 each is returned as well. This information includes:
4260 1. a slightly easier to read version of holdable_formats
4261 2. attributes describing the set of format icons included
4262 in the set of desired, constituent records.
4265 {desc => 'Metarecord ID', type => 'number'},
4266 {desc => 'Context Org ID', type => 'number'},
4267 {desc => 'Hold ID List', type => 'array'},
4271 Stream of objects. The first will have a 'metarecord' key
4272 containing non-hold-specific metarecord information, subsequent
4273 responses will contain a 'hold' key containing hold-specific
4281 sub mr_hold_filter_attrs {
4282 my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
4283 my $e = new_editor();
4285 # by default, return MR / hold attributes for all constituent
4286 # records with holdable copies. If there is a hard boundary,
4287 # though, limit to records with copies within the boundary,
4288 # since anything outside the boundary can never be held.
4291 $org_depth = $U->ou_ancestor_setting_value(
4292 $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
4295 # get all org-scoped records w/ holdable copies for this metarecord
4296 my ($bre_ids) = $self->method_lookup(
4297 'open-ils.circ.holds.metarecord.filtered_records')->run(
4298 $mr_id, undef, $org_id, $org_depth);
4300 my $item_lang_attr = 'item_lang'; # configurable?
4301 my $format_attr = $e->retrieve_config_global_flag(
4302 'opac.metarecord.holds.format_attr')->value;
4304 # helper sub for fetching ccvms for a batch of record IDs
4305 sub get_batch_ccvms {
4306 my ($e, $attr, $bre_ids) = @_;
4307 return [] unless $bre_ids and @$bre_ids;
4308 my $vals = $e->search_metabib_record_attr_flat({
4312 return [] unless @$vals;
4313 return $e->search_config_coded_value_map({
4315 code => [map {$_->value} @$vals]
4319 my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
4320 my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
4325 formats => $formats,
4330 return unless $hold_ids;
4331 my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
4332 $icon_attr = $icon_attr ? $icon_attr->value : '';
4334 for my $hold_id (@$hold_ids) {
4335 my $hold = $e->retrieve_action_hold_request($hold_id)
4336 or return $e->event;
4338 next unless $hold->hold_type eq 'M';
4348 # collect the ccvm's for the selected formats / language
4349 # (i.e. the holdable formats) on the MR.
4350 # this assumes a two-key structure for format / language,
4351 # though no assumption is made about the keys themselves.
4352 my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
4354 my $format_vals = [];
4355 for my $val (values %$hformats) {
4356 # val is either a single ccvm or an array of them
4357 $val = [$val] unless ref $val eq 'ARRAY';
4358 for my $node (@$val) {
4359 push (@$lang_vals, $node->{_val})
4360 if $node->{_attr} eq $item_lang_attr;
4361 push (@$format_vals, $node->{_val})
4362 if $node->{_attr} eq $format_attr;
4366 # fetch the ccvm's for consistency with the {metarecord} blob
4367 $resp->{hold}{formats} = $e->search_config_coded_value_map({
4368 ctype => $format_attr, code => $format_vals});
4369 $resp->{hold}{langs} = $e->search_config_coded_value_map({
4370 ctype => $item_lang_attr, code => $lang_vals});
4372 # find all of the bib records within this metarcord whose
4373 # format / language match the holdable formats on the hold
4374 my ($bre_ids) = $self->method_lookup(
4375 'open-ils.circ.holds.metarecord.filtered_records')->run(
4376 $hold->target, $hold->holdable_formats,
4377 $hold->selection_ou, $hold->selection_depth);
4379 # now find all of the 'icon' attributes for the records
4380 $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
4381 $client->respond($resp);