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 my $apputils = "OpenILS::Application::AppUtils";
43 __PACKAGE__->register_method(
44 method => "create_hold",
45 api_name => "open-ils.circ.holds.create",
47 Create a new hold for an item. From a permissions perspective,
48 the login session is used as the 'requestor' of the hold.
49 The hold recipient is determined by the 'usr' setting within
52 First we verify the requestion has holds request permissions.
53 Then we verify that the recipient is allowed to make the given hold.
54 If not, we see if the requestor has "override" capabilities. If not,
55 a permission exception is returned. If permissions allow, we cycle
56 through the set of holds objects and create.
58 If the recipient does not have permission to place multiple holds
59 on a single title and said operation is attempted, a permission
64 __PACKAGE__->register_method(
65 method => "create_hold",
66 api_name => "open-ils.circ.holds.create.override",
68 If the recipient is not allowed to receive the requested hold,
69 call this method to attempt the override
70 @see open-ils.circ.holds.create
75 my( $self, $conn, $auth, $hold ) = @_;
76 my $e = new_editor(authtoken=>$auth, xact=>1);
77 return $e->event unless $e->checkauth;
79 return -1 unless $hold;
80 my $override = 1 if $self->api_name =~ /override/;
84 my $requestor = $e->requestor;
85 my $recipient = $requestor;
87 if( $requestor->id ne $hold->usr ) {
88 # Make sure the requestor is allowed to place holds for
89 # the recipient if they are not the same people
90 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
91 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
94 # Now make sure the recipient is allowed to receive the specified hold
96 my $porg = $recipient->home_ou;
97 my $rid = $e->requestor->id;
98 my $t = $hold->hold_type;
100 # See if a duplicate hold already exists
102 usr => $recipient->id,
104 fulfillment_time => undef,
105 target => $hold->target,
106 cancel_time => undef,
109 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
111 my $existing = $e->search_action_hold_request($sargs);
112 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
114 my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
115 push( @events, OpenILS::Event->new('HOLD_ITEM_CHECKED_OUT')) if $checked_out;
117 if( $t eq OILS_HOLD_TYPE_METARECORD )
118 { $pevt = $e->event unless $e->allowed('MR_HOLDS', $porg); }
120 if( $t eq OILS_HOLD_TYPE_TITLE )
121 { $pevt = $e->event unless $e->allowed('TITLE_HOLDS', $porg); }
123 if( $t eq OILS_HOLD_TYPE_VOLUME )
124 { $pevt = $e->event unless $e->allowed('VOLUME_HOLDS', $porg); }
126 if( $t eq OILS_HOLD_TYPE_COPY )
127 { $pevt = $e->event unless $e->allowed('COPY_HOLDS', $porg); }
129 return $pevt if $pevt;
133 for my $evt (@events) {
135 my $name = $evt->{textcode};
136 return $e->event unless $e->allowed("$name.override", $porg);
143 # set the configured expire time
144 unless($hold->expire_time) {
145 my $interval = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_HOLD_EXPIRE);
147 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
148 $hold->expire_time($U->epoch2ISO8601($date->epoch));
152 $hold->requestor($e->requestor->id);
153 $hold->request_lib($e->requestor->ws_ou);
154 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
155 $hold = $e->create_action_hold_request($hold) or return $e->event;
159 $conn->respond_complete($hold->id);
162 'open-ils.storage.action.hold_request.copy_targeter',
163 undef, $hold->id ) unless $U->is_true($hold->frozen);
169 my( $self, $client, $login_session, @holds) = @_;
171 if(!@holds){return 0;}
172 my( $user, $evt ) = $apputils->checkses($login_session);
176 if(ref($holds[0]) eq 'ARRAY') {
178 } else { $holds = [ @holds ]; }
180 $logger->debug("Iterating over holds requests...");
182 for my $hold (@$holds) {
185 my $type = $hold->hold_type;
187 $logger->activity("User " . $user->id .
188 " creating new hold of type $type for user " . $hold->usr);
191 if($user->id ne $hold->usr) {
192 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
202 # am I allowed to place holds for this user?
203 if($hold->requestor ne $hold->usr) {
204 $perm = _check_request_holds_perm($user->id, $user->home_ou);
205 if($perm) { return $perm; }
208 # is this user allowed to have holds of this type?
209 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
210 return $perm if $perm;
212 #enforce the fact that the login is the one requesting the hold
213 $hold->requestor($user->id);
214 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
216 my $resp = $apputils->simplereq(
218 'open-ils.storage.direct.action.hold_request.create', $hold );
221 return OpenSRF::EX::ERROR ("Error creating hold");
228 # makes sure that a user has permission to place the type of requested hold
229 # returns the Perm exception if not allowed, returns undef if all is well
230 sub _check_holds_perm {
231 my($type, $user_id, $org_id) = @_;
235 if($evt = $apputils->check_perms(
236 $user_id, $org_id, "MR_HOLDS")) {
240 } elsif ($type eq "T") {
241 if($evt = $apputils->check_perms(
242 $user_id, $org_id, "TITLE_HOLDS")) {
246 } elsif($type eq "V") {
247 if($evt = $apputils->check_perms(
248 $user_id, $org_id, "VOLUME_HOLDS")) {
252 } elsif($type eq "C") {
253 if($evt = $apputils->check_perms(
254 $user_id, $org_id, "COPY_HOLDS")) {
262 # tests if the given user is allowed to place holds on another's behalf
263 sub _check_request_holds_perm {
266 if(my $evt = $apputils->check_perms(
267 $user_id, $org_id, "REQUEST_HOLDS")) {
272 __PACKAGE__->register_method(
273 method => "retrieve_holds_by_id",
274 api_name => "open-ils.circ.holds.retrieve_by_id",
276 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
277 different from the user, then the requestor must have VIEW_HOLD permissions.
281 sub retrieve_holds_by_id {
282 my($self, $client, $auth, $hold_id) = @_;
283 my $e = new_editor(authtoken=>$auth);
284 $e->checkauth or return $e->event;
285 $e->allowed('VIEW_HOLD') or return $e->event;
287 my $holds = $e->search_action_hold_request(
289 { id => $hold_id , fulfillment_time => undef },
291 order_by => { ahr => "request_time" },
293 flesh_fields => {ahr => ['notes']}
298 flesh_hold_transits($holds);
299 flesh_hold_notices($holds, $e);
304 __PACKAGE__->register_method(
305 method => "retrieve_holds",
306 api_name => "open-ils.circ.holds.retrieve",
308 Retrieves all the holds, with hold transits attached, for the specified
309 user id. The login session is the requestor and if the requestor is
310 different from the user, then the requestor must have VIEW_HOLD permissions.
313 __PACKAGE__->register_method(
314 method => "retrieve_holds",
316 api_name => "open-ils.circ.holds.id_list.retrieve",
318 Retrieves all the hold ids for the specified
319 user id. The login session is the requestor and if the requestor is
320 different from the user, then the requestor must have VIEW_HOLD permissions.
324 __PACKAGE__->register_method(
325 method => "retrieve_holds",
327 api_name => "open-ils.circ.holds.canceled.retrieve",
330 __PACKAGE__->register_method(
331 method => "retrieve_holds",
333 api_name => "open-ils.circ.holds.canceled.id_list.retrieve",
338 my($self, $client, $auth, $user_id, $options) = @_;
340 my $e = new_editor(authtoken=>$auth);
341 return $e->event unless $e->checkauth;
342 $user_id = $e->requestor->id unless defined $user_id;
345 unless($user_id == $e->requestor->id) {
346 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
347 unless($e->allowed('VIEW_HOLD', $user->home_ou)) {
348 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
349 $e, $user_id, $e->requestor->id, 'hold.view');
350 return $e->event unless $allowed;
356 if($self->api_name !~ /canceled/) {
358 # Fetch the active holds
360 $holds = $e->search_action_hold_request([
362 fulfillment_time => undef,
363 cancel_time => undef,
365 {order_by => {ahr => "request_time"}}
370 # Fetch the canceled holds
374 $U->ou_ancestor_setting_value(
375 $e->requestor->ws_ou, 'circ.holds.canceled.display_count', $e);
377 unless($cancel_count) {
378 $cancel_age = $U->ou_ancestor_setting_value(
379 $e->requestor->ws_ou, 'circ.holds.canceled.display_age', $e);
382 if($cancel_count) { # limit by count
384 # find at most cancel_count canceled holds
385 $holds = $e->search_action_hold_request([
387 fulfillment_time => undef,
388 cancel_time => {'!=' => undef},
390 {order_by => {ahr => "cancel_time desc"}, limit => $cancel_count}
393 } elsif($cancel_age) { # limit by age
395 # find all of the canceled holds that were canceled within the configured time frame
396 my $date = DateTime->now->subtract(seconds => OpenSRF::Utils::interval_to_seconds($cancel_age));
397 $date = $U->epoch2ISO8601($date->epoch);
399 $holds = $e->search_action_hold_request([
401 fulfillment_time => undef,
402 cancel_time => {'>=' => $date},
404 {order_by => {ahr => "cancel_time desc"}}
409 if( ! $self->api_name =~ /id_list/ ) {
410 for my $hold ( @$holds ) {
412 $e->search_action_hold_transit_copy([
414 {order_by => {ahtc => 'id desc'}, limit => 1}])->[0]
419 if( $self->api_name =~ /id_list/ ) {
420 return [ map { $_->id } @$holds ];
427 __PACKAGE__->register_method(
428 method => 'user_hold_count',
429 api_name => 'open-ils.circ.hold.user.count');
431 sub user_hold_count {
432 my( $self, $conn, $auth, $userid ) = @_;
433 my $e = new_editor(authtoken=>$auth);
434 return $e->event unless $e->checkauth;
435 my $patron = $e->retrieve_actor_user($userid)
437 return $e->event unless $e->allowed('VIEW_HOLD', $patron->home_ou);
438 return __user_hold_count($self, $e, $userid);
441 sub __user_hold_count {
442 my( $self, $e, $userid ) = @_;
443 my $holds = $e->search_action_hold_request(
445 fulfillment_time => undef,
446 cancel_time => undef,
451 return scalar(@$holds);
455 __PACKAGE__->register_method(
456 method => "retrieve_holds_by_pickup_lib",
457 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
459 Retrieves all the holds, with hold transits attached, for the specified
463 __PACKAGE__->register_method(
464 method => "retrieve_holds_by_pickup_lib",
465 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
467 Retrieves all the hold ids for the specified
471 sub retrieve_holds_by_pickup_lib {
472 my($self, $client, $login_session, $ou_id) = @_;
474 #FIXME -- put an appropriate permission check here
475 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
476 # $login_session, $user_id, 'VIEW_HOLD' );
477 #return $evt if $evt;
479 my $holds = $apputils->simplereq(
481 "open-ils.cstore.direct.action.hold_request.search.atomic",
483 pickup_lib => $ou_id ,
484 fulfillment_time => undef,
487 { order_by => { ahr => "request_time" } });
490 if( ! $self->api_name =~ /id_list/ ) {
491 flesh_hold_transits($holds);
494 if( $self->api_name =~ /id_list/ ) {
495 return [ map { $_->id } @$holds ];
502 __PACKAGE__->register_method(
503 method => "uncancel_hold",
504 api_name => "open-ils.circ.hold.uncancel"
508 my($self, $client, $auth, $hold_id) = @_;
509 my $e = new_editor(authtoken=>$auth, xact=>1);
510 return $e->event unless $e->checkauth;
512 my $hold = $e->retrieve_action_hold_request($hold_id)
513 or return $e->die_event;
514 return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
516 return 0 if $hold->fulfillment_time;
517 return 1 unless $hold->cancel_time;
519 # if configured to reset the request time, also reset the expire time
520 if($U->ou_ancestor_setting_value(
521 $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
523 $hold->request_time('now');
524 my $interval = $U->ou_ancestor_setting_value($hold->request_lib, OILS_SETTING_HOLD_EXPIRE);
526 my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
527 $hold->expire_time($U->epoch2ISO8601($date->epoch));
531 $hold->clear_cancel_time;
532 $hold->clear_cancel_cause;
533 $hold->clear_cancel_note;
534 $e->update_action_hold_request($hold) or return $e->die_event;
537 $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $hold_id);
543 __PACKAGE__->register_method(
544 method => "cancel_hold",
545 api_name => "open-ils.circ.hold.cancel",
547 Cancels the specified hold. The login session
548 is the requestor and if the requestor is different from the usr field
549 on the hold, the requestor must have CANCEL_HOLDS permissions.
550 the hold may be either the hold object or the hold id
554 my($self, $client, $auth, $holdid, $cause, $note) = @_;
556 my $e = new_editor(authtoken=>$auth, xact=>1);
557 return $e->event unless $e->checkauth;
559 my $hold = $e->retrieve_action_hold_request($holdid)
562 if( $e->requestor->id ne $hold->usr ) {
563 return $e->event unless $e->allowed('CANCEL_HOLDS');
566 return 1 if $hold->cancel_time;
568 # If the hold is captured, reset the copy status
569 if( $hold->capture_time and $hold->current_copy ) {
571 my $copy = $e->retrieve_asset_copy($hold->current_copy)
574 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
575 $logger->info("canceling hold $holdid whose item is on the holds shelf");
576 # $logger->info("setting copy to status 'reshelving' on hold cancel");
577 # $copy->status(OILS_COPY_STATUS_RESHELVING);
578 # $copy->editor($e->requestor->id);
579 # $copy->edit_date('now');
580 # $e->update_asset_copy($copy) or return $e->event;
582 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
585 $logger->warn("! canceling hold [$hid] that is in transit");
586 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
589 my $trans = $e->retrieve_action_transit_copy($transid);
590 # Leave the transit alive, but set the copy status to
591 # reshelving so it will be properly reshelved when it gets back home
593 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
594 $e->update_action_transit_copy($trans) or return $e->die_event;
600 $hold->cancel_time('now');
601 $hold->cancel_cause($cause);
602 $hold->cancel_note($note);
603 $e->update_action_hold_request($hold)
606 delete_hold_copy_maps($self, $e, $hold->id);
612 sub delete_hold_copy_maps {
617 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
619 $editor->delete_action_hold_copy_map($_)
620 or return $editor->event;
626 __PACKAGE__->register_method(
627 method => "update_hold",
628 api_name => "open-ils.circ.hold.update",
630 Updates the specified hold. The login session
631 is the requestor and if the requestor is different from the usr field
632 on the hold, the requestor must have UPDATE_HOLDS permissions.
635 __PACKAGE__->register_method(
636 method => "batch_update_hold",
637 api_name => "open-ils.circ.hold.update.batch",
640 Updates the specified hold. The login session
641 is the requestor and if the requestor is different from the usr field
642 on the hold, the requestor must have UPDATE_HOLDS permissions.
646 my($self, $client, $auth, $hold, $values) = @_;
647 my $e = new_editor(authtoken=>$auth, xact=>1);
648 return $e->die_event unless $e->checkauth;
649 my $resp = update_hold_impl($self, $e, $hold, $values);
650 return $resp if $U->event_code($resp);
655 sub batch_update_hold {
656 my($self, $client, $auth, $hold_list, $values_list) = @_;
657 my $e = new_editor(authtoken=>$auth);
658 return $e->die_event unless $e->checkauth;
660 my $count = ($hold_list) ? scalar(@$hold_list) : scalar(@$values_list);
664 for my $idx (0..$count-1) {
666 my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
667 $e->xact_commit unless $U->event_code($resp);
668 $client->respond($resp);
675 sub update_hold_impl {
676 my($self, $e, $hold, $values) = @_;
679 $hold = $e->retrieve_action_hold_request($values->{id})
680 or return $e->die_event;
681 $hold->$_($values->{$_}) for keys %$values;
684 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
685 or return $e->die_event;
687 # don't allow the user to be changed
688 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
690 if($hold->usr ne $e->requestor->id) {
691 # if the hold is for a different user, make sure the
692 # requestor has the appropriate permissions
693 my $usr = $e->retrieve_actor_user($hold->usr)
694 or return $e->die_event;
695 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
699 # --------------------------------------------------------------
700 # Changing the request time is like playing God
701 # --------------------------------------------------------------
702 if($hold->request_time ne $orig_hold->request_time) {
703 return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
704 return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
707 # --------------------------------------------------------------
708 # if the hold is on the holds shelf or in transit and the pickup
709 # lib changes we need to create a new transit.
710 # --------------------------------------------------------------
711 if($orig_hold->pickup_lib ne $hold->pickup_lib) {
713 my $status = _hold_status($e, $hold);
715 if($status == 3) { # in transit
717 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
718 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
720 $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
722 # update the transit to reflect the new pickup location
723 my $transit = $e->search_action_hold_transit_copy(
724 {hold=>$hold->id, dest_recv_time => undef})->[0]
725 or return $e->die_event;
727 $transit->prev_dest($transit->dest); # mark the previous destination on the transit
728 $transit->dest($hold->pickup_lib);
729 $e->update_action_hold_transit_copy($transit) or return $e->die_event;
731 } elsif($status == 4) { # on holds shelf
733 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
734 return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
736 $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
738 # create the new transit
739 my $evt = transit_hold($e, $orig_hold, $hold, $e->retrieve_asset_copy($hold->current_copy));
744 update_hold_if_frozen($self, $e, $hold, $orig_hold);
745 $e->update_action_hold_request($hold) or return $e->die_event;
748 # a change to mint-condition changes the set of potential copies, so retarget the hold;
749 if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
750 _reset_hold($self, $e->requestor, $hold)
757 my($e, $orig_hold, $hold, $copy) = @_;
758 my $src = $orig_hold->pickup_lib;
759 my $dest = $hold->pickup_lib;
761 $logger->info("putting hold into transit on pickup_lib update");
763 my $transit = Fieldmapper::action::hold_transit_copy->new;
764 $transit->hold($hold->id);
765 $transit->source($src);
766 $transit->dest($dest);
767 $transit->target_copy($copy->id);
768 $transit->source_send_time('now');
769 $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
771 $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
772 $copy->editor($e->requestor->id);
773 $copy->edit_date('now');
775 $e->create_action_hold_transit_copy($transit) or return $e->die_event;
776 $e->update_asset_copy($copy) or return $e->die_event;
780 # if the hold is frozen, this method ensures that the hold is not "targeted",
781 # that is, it clears the current_copy and prev_check_time to essentiallly
782 # reset the hold. If it is being activated, it runs the targeter in the background
783 sub update_hold_if_frozen {
784 my($self, $e, $hold, $orig_hold) = @_;
785 return if $hold->capture_time;
787 if($U->is_true($hold->frozen)) {
788 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
789 $hold->clear_current_copy;
790 $hold->clear_prev_check_time;
793 if($U->is_true($orig_hold->frozen)) {
794 $logger->info("Running targeter on activated hold ".$hold->id);
795 $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
799 __PACKAGE__->register_method(
800 method => "hold_note_CUD",
801 api_name => "open-ils.circ.hold_request.note.cud");
804 my($self, $conn, $auth, $note) = @_;
806 my $e = new_editor(authtoken => $auth, xact => 1);
807 return $e->die_event unless $e->checkauth;
809 my $hold = $e->retrieve_action_hold_request($note->hold)
810 or return $e->die_event;
812 if($hold->usr ne $e->requestor->id) {
813 my $usr = $e->retrieve_actor_user($hold->usr);
814 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
815 $note->staff('t') if $note->isnew;
819 $e->create_action_hold_request_note($note) or return $e->die_event;
820 } elsif($note->ischanged) {
821 $e->update_action_hold_request_note($note) or return $e->die_event;
822 } elsif($note->isdeleted) {
823 $e->delete_action_hold_request_note($note) or return $e->die_event;
832 __PACKAGE__->register_method(
833 method => "retrieve_hold_status",
834 api_name => "open-ils.circ.hold.status.retrieve",
836 Calculates the current status of the hold.
837 the requestor must have VIEW_HOLD permissions if the hold is for a user
838 other than the requestor.
839 Returns -1 on error (for now)
840 Returns 1 for 'waiting for copy to become available'
841 Returns 2 for 'waiting for copy capture'
842 Returns 3 for 'in transit'
843 Returns 4 for 'arrived'
844 Returns 5 for 'hold-shelf-delay'
847 sub retrieve_hold_status {
848 my($self, $client, $auth, $hold_id) = @_;
850 my $e = new_editor(authtoken => $auth);
851 return $e->event unless $e->checkauth;
852 my $hold = $e->retrieve_action_hold_request($hold_id)
855 if( $e->requestor->id != $hold->usr ) {
856 return $e->event unless $e->allowed('VIEW_HOLD');
859 return _hold_status($e, $hold);
865 return 1 unless $hold->current_copy;
866 return 2 unless $hold->capture_time;
868 my $copy = $hold->current_copy;
869 unless( ref $copy ) {
870 $copy = $e->retrieve_asset_copy($hold->current_copy)
874 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
876 if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
878 my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
879 return 4 unless $hs_wait_interval;
881 # if a hold_shelf_status_delay interval is defined and start_time plus
882 # the interval is greater than now, consider the hold to be in the virtual
883 # "on its way to the holds shelf" status. Return 5.
885 my $transit = $e->search_action_hold_transit_copy({hold => $hold->id})->[0];
886 my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
887 $start_time = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time));
888 my $end_time = $start_time->add(seconds => OpenSRF::Utils::interval_to_seconds($hs_wait_interval));
890 return 5 if $end_time > DateTime->now;
899 __PACKAGE__->register_method(
900 method => "retrieve_hold_queue_stats",
901 api_name => "open-ils.circ.hold.queue_stats.retrieve",
904 Returns object with total_holds count, queue_position, potential_copies count, and status code
909 sub retrieve_hold_queue_stats {
910 my($self, $conn, $auth, $hold_id) = @_;
911 my $e = new_editor(authtoken => $auth);
912 return $e->event unless $e->checkauth;
913 my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
914 if($e->requestor->id != $hold->usr) {
915 return $e->event unless $e->allowed('VIEW_HOLD');
917 return retrieve_hold_queue_status_impl($e, $hold);
920 sub retrieve_hold_queue_status_impl {
924 # The holds queue is defined as the set of holds that share at
925 # least one potential copy with the context hold
926 my $q_holds = $e->json_query({
929 # fetch request_time since it's in the order_by and we're asking for distinct values
930 ahr => ['request_time']
932 from => {ahcm => 'ahr'},
933 order_by => {ahr => ['request_time']},
938 select => {ahcm => ['target_copy']},
940 where => {hold => $hold->id}
947 for my $h (@$q_holds) {
948 last if $h->{hold} == $hold->id;
952 # total count of potential copies
953 my $num_potentials = $e->json_query({
954 select => {ahcm => [{column => 'id', transform => 'count', alias => 'count'}]},
956 where => {hold => $hold->id}
959 my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
960 my $default_hold_interval = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
961 my $estimated_wait = $qpos * ($default_hold_interval / $num_potentials) if $default_hold_interval;
964 total_holds => scalar(@$q_holds),
965 queue_position => $qpos,
966 potential_copies => $num_potentials->{count},
967 status => _hold_status($e, $hold),
968 estimated_wait => int($estimated_wait)
973 sub fetch_open_hold_by_current_copy {
976 my $hold = $apputils->simplereq(
978 'open-ils.cstore.direct.action.hold_request.search.atomic',
979 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
980 return $hold->[0] if ref($hold);
984 sub fetch_related_holds {
987 return $apputils->simplereq(
989 'open-ils.cstore.direct.action.hold_request.search.atomic',
990 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
994 __PACKAGE__->register_method (
995 method => "hold_pull_list",
996 api_name => "open-ils.circ.hold_pull_list.retrieve",
998 Returns a list of holds that need to be "pulled"
1003 __PACKAGE__->register_method (
1004 method => "hold_pull_list",
1005 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
1007 Returns a list of hold ID's that need to be "pulled"
1012 __PACKAGE__->register_method (
1013 method => "hold_pull_list",
1014 api_name => "open-ils.circ.hold_pull_list.retrieve.count",
1016 Returns a list of holds that need to be "pulled"
1022 sub hold_pull_list {
1023 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
1024 my( $reqr, $evt ) = $U->checkses($authtoken);
1025 return $evt if $evt;
1027 my $org = $reqr->ws_ou || $reqr->home_ou;
1028 # the perm locaiton shouldn't really matter here since holds
1029 # will exist all over and VIEW_HOLDS should be universal
1030 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
1031 return $evt if $evt;
1033 if($self->api_name =~ /count/) {
1035 my $count = $U->storagereq(
1036 'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
1037 $org, $limit, $offset );
1039 $logger->info("Grabbing pull list for org unit $org with $count items");
1042 } elsif( $self->api_name =~ /id_list/ ) {
1043 return $U->storagereq(
1044 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
1045 $org, $limit, $offset );
1048 return $U->storagereq(
1049 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
1050 $org, $limit, $offset );
1054 __PACKAGE__->register_method (
1055 method => 'fetch_hold_notify',
1056 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
1059 Returns a list of hold notification objects based on hold id.
1060 @param authtoken The loggin session key
1061 @param holdid The id of the hold whose notifications we want to retrieve
1062 @return An array of hold notification objects, event on error.
1066 sub fetch_hold_notify {
1067 my( $self, $conn, $authtoken, $holdid ) = @_;
1068 my( $requestor, $evt ) = $U->checkses($authtoken);
1069 return $evt if $evt;
1070 my ($hold, $patron);
1071 ($hold, $evt) = $U->fetch_hold($holdid);
1072 return $evt if $evt;
1073 ($patron, $evt) = $U->fetch_user($hold->usr);
1074 return $evt if $evt;
1076 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
1077 return $evt if $evt;
1079 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
1080 return $U->cstorereq(
1081 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
1085 __PACKAGE__->register_method (
1086 method => 'create_hold_notify',
1087 api_name => 'open-ils.circ.hold_notification.create',
1089 Creates a new hold notification object
1090 @param authtoken The login session key
1091 @param notification The hold notification object to create
1092 @return ID of the new object on success, Event on error
1096 sub create_hold_notify {
1097 my( $self, $conn, $auth, $note ) = @_;
1098 my $e = new_editor(authtoken=>$auth, xact=>1);
1099 return $e->die_event unless $e->checkauth;
1101 my $hold = $e->retrieve_action_hold_request($note->hold)
1102 or return $e->die_event;
1103 my $patron = $e->retrieve_actor_user($hold->usr)
1104 or return $e->die_event;
1106 return $e->die_event unless
1107 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
1109 $note->notify_staff($e->requestor->id);
1110 $e->create_action_hold_notification($note) or return $e->die_event;
1115 __PACKAGE__->register_method (
1116 method => 'create_hold_note',
1117 api_name => 'open-ils.circ.hold_note.create',
1119 Creates a new hold request note object
1120 @param authtoken The login session key
1121 @param note The hold note object to create
1122 @return ID of the new object on success, Event on error
1126 sub create_hold_note {
1127 my( $self, $conn, $auth, $note ) = @_;
1128 my $e = new_editor(authtoken=>$auth, xact=>1);
1129 return $e->die_event unless $e->checkauth;
1131 my $hold = $e->retrieve_action_hold_request($note->hold)
1132 or return $e->die_event;
1133 my $patron = $e->retrieve_actor_user($hold->usr)
1134 or return $e->die_event;
1136 return $e->die_event unless
1137 $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn. Probably want something more specific
1139 $e->create_action_hold_request_note($note) or return $e->die_event;
1144 __PACKAGE__->register_method(
1145 method => 'reset_hold',
1146 api_name => 'open-ils.circ.hold.reset',
1148 Un-captures and un-targets a hold, essentially returning
1149 it to the state it was in directly after it was placed,
1150 then attempts to re-target the hold
1151 @param authtoken The login session key
1152 @param holdid The id of the hold
1158 my( $self, $conn, $auth, $holdid ) = @_;
1160 my ($hold, $evt) = $U->fetch_hold($holdid);
1161 return $evt if $evt;
1162 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
1163 return $evt if $evt;
1164 $evt = _reset_hold($self, $reqr, $hold);
1165 return $evt if $evt;
1170 __PACKAGE__->register_method(
1171 method => 'reset_hold_batch',
1172 api_name => 'open-ils.circ.hold.reset.batch'
1175 sub reset_hold_batch {
1176 my($self, $conn, $auth, $hold_ids) = @_;
1178 my $e = new_editor(authtoken => $auth);
1179 return $e->event unless $e->checkauth;
1181 for my $hold_id ($hold_ids) {
1183 my $hold = $e->retrieve_action_hold_request(
1184 [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
1185 or return $e->event;
1187 next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
1188 _reset_hold($self, $e->requestor, $hold);
1196 my ($self, $reqr, $hold) = @_;
1198 my $e = new_editor(xact =>1, requestor => $reqr);
1200 $logger->info("reseting hold ".$hold->id);
1202 my $hid = $hold->id;
1204 if( $hold->capture_time and $hold->current_copy ) {
1206 my $copy = $e->retrieve_asset_copy($hold->current_copy)
1207 or return $e->event;
1209 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
1210 $logger->info("setting copy to status 'reshelving' on hold retarget");
1211 $copy->status(OILS_COPY_STATUS_RESHELVING);
1212 $copy->editor($e->requestor->id);
1213 $copy->edit_date('now');
1214 $e->update_asset_copy($copy) or return $e->event;
1216 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
1218 # We don't want the copy to remain "in transit"
1219 $copy->status(OILS_COPY_STATUS_RESHELVING);
1220 $logger->warn("! reseting hold [$hid] that is in transit");
1221 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
1224 my $trans = $e->retrieve_action_transit_copy($transid);
1226 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
1227 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
1228 $logger->info("Transit abort completed with result $evt");
1229 return $evt unless "$evt" eq 1;
1235 $hold->clear_capture_time;
1236 $hold->clear_current_copy;
1237 $hold->clear_shelf_time;
1238 $hold->clear_shelf_expire_time;
1240 $e->update_action_hold_request($hold) or return $e->event;
1244 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
1250 __PACKAGE__->register_method(
1251 method => 'fetch_open_title_holds',
1252 api_name => 'open-ils.circ.open_holds.retrieve',
1254 Returns a list ids of un-fulfilled holds for a given title id
1255 @param authtoken The login session key
1256 @param id the id of the item whose holds we want to retrieve
1257 @param type The hold type - M, T, V, C
1261 sub fetch_open_title_holds {
1262 my( $self, $conn, $auth, $id, $type, $org ) = @_;
1263 my $e = new_editor( authtoken => $auth );
1264 return $e->event unless $e->checkauth;
1267 $org ||= $e->requestor->ws_ou;
1269 # return $e->search_action_hold_request(
1270 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
1272 # XXX make me return IDs in the future ^--
1273 my $holds = $e->search_action_hold_request(
1276 cancel_time => undef,
1278 fulfillment_time => undef
1282 flesh_hold_transits($holds);
1287 sub flesh_hold_transits {
1289 for my $hold ( @$holds ) {
1291 $apputils->simplereq(
1293 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
1294 { hold => $hold->id },
1295 { order_by => { ahtc => 'id desc' }, limit => 1 }
1301 sub flesh_hold_notices {
1302 my( $holds, $e ) = @_;
1303 $e ||= new_editor();
1305 for my $hold (@$holds) {
1306 my $notices = $e->search_action_hold_notification(
1308 { hold => $hold->id },
1309 { order_by => { anh => 'notify_time desc' } },
1314 $hold->notify_count(scalar(@$notices));
1316 my $n = $e->retrieve_action_hold_notification($$notices[0])
1317 or return $e->event;
1318 $hold->notify_time($n->notify_time);
1324 __PACKAGE__->register_method(
1325 method => 'fetch_captured_holds',
1326 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
1329 Returns a list of un-fulfilled holds for a given title id
1330 @param authtoken The login session key
1331 @param org The org id of the location in question
1335 __PACKAGE__->register_method(
1336 method => 'fetch_captured_holds',
1337 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1340 Returns a list ids of un-fulfilled holds for a given title id
1341 @param authtoken The login session key
1342 @param org The org id of the location in question
1346 sub fetch_captured_holds {
1347 my( $self, $conn, $auth, $org ) = @_;
1349 my $e = new_editor(authtoken => $auth);
1350 return $e->event unless $e->checkauth;
1351 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1353 $org ||= $e->requestor->ws_ou;
1355 my $hold_ids = $e->json_query(
1357 select => { ahr => ['id'] },
1362 fkey => 'current_copy'
1367 '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
1369 capture_time => { "!=" => undef },
1370 current_copy => { "!=" => undef },
1371 fulfillment_time => undef,
1373 cancel_time => undef,
1379 for my $hold_id (@$hold_ids) {
1380 if($self->api_name =~ /id_list/) {
1381 $conn->respond($hold_id->{id});
1385 $e->retrieve_action_hold_request([
1389 flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
1390 order_by => {anh => 'notify_time desc'}
1399 __PACKAGE__->register_method(
1400 method => "check_title_hold",
1401 api_name => "open-ils.circ.title_hold.is_possible",
1403 Determines if a hold were to be placed by a given user,
1404 whether or not said hold would have any potential copies
1406 @param authtoken The login session key
1407 @param params A hash of named params including:
1408 patronid - the id of the hold recipient
1409 titleid (brn) - the id of the title to be held
1410 depth - the hold range depth (defaults to 0)
1413 sub check_title_hold {
1414 my( $self, $client, $authtoken, $params ) = @_;
1416 my %params = %$params;
1417 my $titleid = $params{titleid} ||"";
1418 my $volid = $params{volume_id};
1419 my $copyid = $params{copy_id};
1420 my $mrid = $params{mrid} ||"";
1421 my $depth = $params{depth} || 0;
1422 my $pickup_lib = $params{pickup_lib};
1423 my $hold_type = $params{hold_type} || 'T';
1424 my $selection_ou = $params{selection_ou} || $pickup_lib;
1426 my $e = new_editor(authtoken=>$authtoken);
1427 return $e->event unless $e->checkauth;
1428 my $patron = $e->retrieve_actor_user($params{patronid})
1429 or return $e->event;
1431 if( $e->requestor->id ne $patron->id ) {
1432 return $e->event unless
1433 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1436 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1438 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1439 or return $e->event;
1441 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1442 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1444 if(defined $soft_boundary and $$params{depth} < $soft_boundary) {
1445 # work up the tree and as soon as we find a potential copy, use that depth
1446 # also, make sure we don't go past the hard boundary if it exists
1448 # our min boundary is the greater of user-specified boundary or hard boundary
1449 my $min_depth = (defined $hard_boundary and $hard_boundary > $$params{depth}) ?
1450 $hard_boundary : $$params{depth};
1452 my $depth = $soft_boundary;
1453 while($depth >= $min_depth) {
1454 $logger->info("performing hold possibility check with soft boundary $depth");
1455 my @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1456 return {success => 1, depth => $depth, local_avail => $status[1]} if $status[0];
1459 return {success => 0};
1461 } elsif(defined $hard_boundary and $$params{depth} < $hard_boundary) {
1462 # there is no soft boundary, enforce the hard boundary if it exists
1463 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1464 my @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
1466 return {success => 1, depth => $hard_boundary, local_avail => $status[1]}
1468 return {success => 0};
1472 # no boundaries defined, fall back to user specifed boundary or no boundary
1473 $logger->info("performing hold possibility check with no boundary");
1474 my @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
1476 return {success => 1, depth => $hard_boundary, local_avail => $status[1]};
1478 return {success => 0};
1483 sub do_possibility_checks {
1484 my($e, $patron, $request_lib, $depth, %params) = @_;
1486 my $titleid = $params{titleid} ||"";
1487 my $volid = $params{volume_id};
1488 my $copyid = $params{copy_id};
1489 my $mrid = $params{mrid} ||"";
1490 my $pickup_lib = $params{pickup_lib};
1491 my $hold_type = $params{hold_type} || 'T';
1492 my $selection_ou = $params{selection_ou} || $pickup_lib;
1499 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1501 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1502 $volume = $e->retrieve_asset_call_number($copy->call_number)
1503 or return $e->event;
1504 $title = $e->retrieve_biblio_record_entry($volume->record)
1505 or return $e->event;
1506 return verify_copy_for_hold(
1507 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1509 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1511 $volume = $e->retrieve_asset_call_number($volid)
1512 or return $e->event;
1513 $title = $e->retrieve_biblio_record_entry($volume->record)
1514 or return $e->event;
1516 return _check_volume_hold_is_possible(
1517 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1519 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1521 return _check_title_hold_is_possible(
1522 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1524 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1526 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1527 my @recs = map { $_->source } @$maps;
1528 for my $rec (@recs) {
1529 my @status = _check_title_hold_is_possible(
1530 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1531 return @status if $status[1];
1538 sub create_ranged_org_filter {
1539 my($e, $selection_ou, $depth) = @_;
1541 # find the orgs from which this hold may be fulfilled,
1542 # based on the selection_ou and depth
1544 my $top_org = $e->search_actor_org_unit([
1545 {parent_ou => undef},
1546 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1549 return () if $depth == $top_org->ou_type->depth;
1551 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1552 %org_filter = (circ_lib => []);
1553 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1555 $logger->info("hold org filter at depth $depth and selection_ou ".
1556 "$selection_ou created list of @{$org_filter{circ_lib}}");
1562 sub _check_title_hold_is_possible {
1563 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1565 my $e = new_editor();
1566 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1568 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1569 my $copies = $e->json_query(
1571 select => { acp => ['id', 'circ_lib'] },
1576 fkey => 'call_number',
1580 filter => { id => $titleid },
1585 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1586 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1590 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1595 $logger->info("title possible found ".scalar(@$copies)." potential copies");
1596 return (0) unless @$copies;
1598 # -----------------------------------------------------------------------
1599 # sort the copies into buckets based on their circ_lib proximity to
1600 # the patron's home_ou.
1601 # -----------------------------------------------------------------------
1603 my $home_org = $patron->home_ou;
1604 my $req_org = $request_lib->id;
1606 $logger->info("prox cache $home_org " . $prox_cache{$home_org});
1608 $prox_cache{$home_org} =
1609 $e->search_actor_org_unit_proximity({from_org => $home_org})
1610 unless $prox_cache{$home_org};
1611 my $home_prox = $prox_cache{$home_org};
1614 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1615 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1617 my @keys = sort { $a <=> $b } keys %buckets;
1620 if( $home_org ne $req_org ) {
1621 # -----------------------------------------------------------------------
1622 # shove the copies close to the request_lib into the primary buckets
1623 # directly before the farthest away copies. That way, they are not
1624 # given priority, but they are checked before the farthest copies.
1625 # -----------------------------------------------------------------------
1626 $prox_cache{$req_org} =
1627 $e->search_actor_org_unit_proximity({from_org => $req_org})
1628 unless $prox_cache{$req_org};
1629 my $req_prox = $prox_cache{$req_org};
1633 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1634 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1636 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1637 my $new_key = $highest_key - 0.5; # right before the farthest prox
1638 my @keys2 = sort { $a <=> $b } keys %buckets2;
1639 for my $key (@keys2) {
1640 last if $key >= $highest_key;
1641 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1645 @keys = sort { $a <=> $b } keys %buckets;
1649 for my $key (@keys) {
1650 my @cps = @{$buckets{$key}};
1652 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1654 for my $copyid (@cps) {
1656 next if $seen{$copyid};
1657 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1658 my $copy = $e->retrieve_asset_copy($copyid);
1659 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1661 unless($title) { # grab the title if we don't already have it
1662 my $vol = $e->retrieve_asset_call_number(
1663 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1664 $title = $vol->record;
1667 my @status = verify_copy_for_hold(
1668 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1670 return @status if $status[0];
1678 sub _check_volume_hold_is_possible {
1679 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1680 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1681 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1682 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1683 for my $copy ( @$copies ) {
1684 my @status = verify_copy_for_hold(
1685 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1686 return @status if $status[0];
1693 sub verify_copy_for_hold {
1694 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1695 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1696 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1697 { patron => $patron,
1698 requestor => $requestor,
1701 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1702 pickup_lib => $pickup_lib,
1703 request_lib => $request_lib,
1711 ($copy->circ_lib == $pickup_lib) and
1712 ($copy->status == OILS_COPY_STATUS_AVAILABLE)
1719 sub find_nearest_permitted_hold {
1722 my $editor = shift; # CStoreEditor object
1723 my $copy = shift; # copy to target
1724 my $user = shift; # staff
1725 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1726 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1728 my $bc = $copy->barcode;
1730 # find any existing holds that already target this copy
1731 my $old_holds = $editor->search_action_hold_request(
1732 { current_copy => $copy->id,
1733 cancel_time => undef,
1734 capture_time => undef
1738 # hold->type "R" means we need this copy
1739 for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1742 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1744 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1745 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1747 # search for what should be the best holds for this copy to fulfill
1748 my $best_holds = $U->storagereq(
1749 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1750 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1752 unless(@$best_holds) {
1754 if( my $hold = $$old_holds[0] ) {
1755 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1759 $logger->info("circulator: no suitable holds found for copy $bc");
1760 return (undef, $evt);
1766 # for each potential hold, we have to run the permit script
1767 # to make sure the hold is actually permitted.
1768 for my $holdid (@$best_holds) {
1769 next unless $holdid;
1770 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1772 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1773 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1774 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1776 # see if this hold is permitted
1777 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1778 { patron_id => $hold->usr,
1781 pickup_lib => $hold->pickup_lib,
1782 request_lib => $rlib,
1793 unless( $best_hold ) { # no "good" permitted holds were found
1794 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1795 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1800 $logger->info("circulator: no suitable holds found for copy $bc");
1801 return (undef, $evt);
1804 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1806 # indicate a permitted hold was found
1807 return $best_hold if $check_only;
1809 # we've found a permitted hold. we need to "grab" the copy
1810 # to prevent re-targeted holds (next part) from re-grabbing the copy
1811 $best_hold->current_copy($copy->id);
1812 $editor->update_action_hold_request($best_hold)
1813 or return (undef, $editor->event);
1818 # re-target any other holds that already target this copy
1819 for my $old_hold (@$old_holds) {
1820 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1821 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1822 $old_hold->id." after a better hold [".$best_hold->id."] was found");
1823 $old_hold->clear_current_copy;
1824 $old_hold->clear_prev_check_time;
1825 $editor->update_action_hold_request($old_hold)
1826 or return (undef, $editor->event);
1827 push(@retarget, $old_hold->id);
1830 return ($best_hold, undef, (@retarget) ? \@retarget : undef);
1838 __PACKAGE__->register_method(
1839 method => 'all_rec_holds',
1840 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1844 my( $self, $conn, $auth, $title_id, $args ) = @_;
1846 my $e = new_editor(authtoken=>$auth);
1847 $e->checkauth or return $e->event;
1848 $e->allowed('VIEW_HOLD') or return $e->event;
1851 $args->{fulfillment_time} = undef; # we don't want to see old fulfilled holds
1852 $args->{cancel_time} = undef;
1854 my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
1856 my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
1858 $resp->{metarecord_holds} = $e->search_action_hold_request(
1859 { hold_type => OILS_HOLD_TYPE_METARECORD,
1860 target => $mr_map->metarecord,
1866 $resp->{title_holds} = $e->search_action_hold_request(
1868 hold_type => OILS_HOLD_TYPE_TITLE,
1869 target => $title_id,
1873 my $vols = $e->search_asset_call_number(
1874 { record => $title_id, deleted => 'f' }, {idlist=>1});
1876 return $resp unless @$vols;
1878 $resp->{volume_holds} = $e->search_action_hold_request(
1880 hold_type => OILS_HOLD_TYPE_VOLUME,
1885 my $copies = $e->search_asset_copy(
1886 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1888 return $resp unless @$copies;
1890 $resp->{copy_holds} = $e->search_action_hold_request(
1892 hold_type => OILS_HOLD_TYPE_COPY,
1904 __PACKAGE__->register_method(
1905 method => 'uber_hold',
1907 api_name => 'open-ils.circ.hold.details.retrieve'
1911 my($self, $client, $auth, $hold_id) = @_;
1912 my $e = new_editor(authtoken=>$auth);
1913 $e->checkauth or return $e->event;
1914 return uber_hold_impl($e, $hold_id);
1917 __PACKAGE__->register_method(
1918 method => 'batch_uber_hold',
1921 api_name => 'open-ils.circ.hold.details.batch.retrieve'
1924 sub batch_uber_hold {
1925 my($self, $client, $auth, $hold_ids) = @_;
1926 my $e = new_editor(authtoken=>$auth);
1927 $e->checkauth or return $e->event;
1928 $client->respond(uber_hold_impl($e, $_)) for @$hold_ids;
1932 sub uber_hold_impl {
1933 my($e, $hold_id) = @_;
1937 my $hold = $e->retrieve_action_hold_request(
1942 flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
1945 ) or return $e->event;
1947 if($hold->usr->id ne $e->requestor->id) {
1948 # A user is allowed to see his/her own holds
1949 $e->allowed('VIEW_HOLD') or return $e->event;
1952 my $user = $hold->usr;
1953 $hold->usr($user->id);
1955 my $card = $e->retrieve_actor_card($user->card)
1956 or return $e->event;
1958 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1960 flesh_hold_notices([$hold], $e);
1961 flesh_hold_transits([$hold]);
1963 my $details = retrieve_hold_queue_status_impl($e, $hold);
1970 patron_first => $user->first_given_name,
1971 patron_last => $user->family_name,
1972 patron_barcode => $card->barcode,
1979 # -----------------------------------------------------
1980 # Returns the MVR object that represents what the
1982 # -----------------------------------------------------
1984 my( $e, $hold ) = @_;
1990 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1991 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1992 or return $e->event;
1993 $tid = $mr->master_record;
1995 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1996 $tid = $hold->target;
1998 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1999 $volume = $e->retrieve_asset_call_number($hold->target)
2000 or return $e->event;
2001 $tid = $volume->record;
2003 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
2004 $copy = $e->retrieve_asset_copy($hold->target)
2005 or return $e->event;
2006 $volume = $e->retrieve_asset_call_number($copy->call_number)
2007 or return $e->event;
2008 $tid = $volume->record;
2011 if(!$copy and ref $hold->current_copy ) {
2012 $copy = $hold->current_copy;
2013 $hold->current_copy($copy->id);
2016 if(!$volume and $copy) {
2017 $volume = $e->retrieve_asset_call_number($copy->call_number);
2020 # TODO return metarcord mvr for M holds
2021 my $title = $e->retrieve_biblio_record_entry($tid);
2022 return ( $U->record_to_mvr($title), $volume, $copy );
2026 __PACKAGE__->register_method(
2027 method => 'clear_shelf_process',
2029 api_name => 'open-ils.circ.hold.clear_shelf.process',
2032 1. Find all holds that have expired on the holds shelf
2034 3. If a clear-shelf status is configured, put targeted copies into this status
2035 4. Divide copies into 3 groups: items to transit, items to reshelve, and items
2036 that are needed for holds. No subsequent action is taken on the holds
2037 or items after grouping.
2042 sub clear_shelf_process {
2043 my($self, $client, $auth, $org_id) = @_;
2045 my $e = new_editor(authtoken=>$auth, xact => 1);
2046 $e->checkauth or return $e->die_event;
2048 $org_id ||= $e->requestor->ws_ou;
2049 $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
2051 my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
2053 # Find holds on the shelf that have been there too long
2054 my $hold_ids = $e->search_action_hold_request(
2055 { shelf_expire_time => {'<' => 'now'},
2056 pickup_lib => $org_id,
2057 cancel_time => undef,
2058 fulfillment_time => undef,
2059 shelf_time => {'!=' => undef}
2066 for my $hold_id (@$hold_ids) {
2068 $logger->info("Clear shelf processing hold $hold_id");
2070 my $hold = $e->retrieve_action_hold_request([
2073 flesh_fields => {ahr => ['current_copy']}
2077 $hold->cancel_time('now');
2078 $hold->cancel_cause(2); # Hold Shelf expiration
2079 $e->update_action_hold_request($hold) or return $e->die_event;
2081 my $copy = $hold->current_copy;
2084 # if a clear-shelf copy status is defined, update the copy
2085 $copy->status($copy_status);
2086 $copy->edit_date('now');
2087 $copy->editor($e->requestor->id);
2088 $e->update_asset_copy($copy) or return $e->die_event;
2091 my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
2095 # copy is needed for a hold
2096 $client->respond({action => 'hold', copy => $copy, hold_id => $hold->id});
2098 } elsif($copy->circ_lib != $e->requestor->ws_ou) {
2100 # copy needs to transit
2101 $client->respond({action => 'transit', copy => $copy, hold_id => $hold->id});
2105 # copy needs to go back to the shelf
2106 $client->respond({action => 'shelf', copy => $copy, hold_id => $hold->id});
2109 push(@holds, $hold);
2114 # tell the client we're done
2115 $client->resopnd_complete;
2117 # fire off the hold cancelation trigger
2118 my $trigger = OpenSRF::AppSession->connect('open-ils.trigger');
2120 for my $hold (@holds) {
2122 my $req = $trigger->request(
2123 'open-ils.trigger.event.autocreate',
2124 'hold_request.cancel.expire_holds_shelf',
2127 # wait for response so don't flood the service
2131 $trigger->disconnect;
2135 __PACKAGE__->register_method(
2136 method => 'usr_hold_summary',
2137 api_name => 'open-ils.circ.holds.user_summary',
2139 Returns a summary of holds statuses for a given user
2143 sub usr_hold_summary {
2144 my($self, $conn, $auth, $user_id) = @_;
2146 my $e = new_editor(authtoken=>$auth);
2147 $e->checkauth or return $e->event;
2148 $e->allowed('VIEW_HOLD') or return $e->event;
2150 my $holds = $e->search_action_hold_request(
2153 fulfillment_time => undef,
2154 cancel_time => undef,
2158 my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
2159 $summary{_hold_status($e, $_)} += 1 for @$holds;
2165 __PACKAGE__->register_method(
2166 method => 'hold_has_copy_at',
2167 api_name => 'open-ils.circ.hold.has_copy_at',
2169 Returns the ID of the found copy and name of the shelving location if there is
2170 an available copy at the specified org unit. Returns empty hash otherwise.
2174 sub hold_has_copy_at {
2175 my($self, $conn, $auth, $args) = @_;
2177 my $e = new_editor(authtoken=>$auth);
2178 $e->checkauth or return $e->event;
2180 my $hold_type = $$args{hold_type};
2181 my $hold_target = $$args{hold_target};
2182 my $org_unit = $$args{org_unit};
2185 select => {acp => ['id'], acpl => ['name']},
2188 acpl => {field => 'id', filter => { holdable => 't'}, fkey => 'location'},
2189 ccs => {field => 'id', filter => { holdable => 't'}, fkey => 'status'}
2192 where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit}},
2196 if($hold_type eq 'C') {
2198 $query->{where}->{'+acp'}->{id} = $hold_target;
2200 } elsif($hold_type eq 'V') {
2202 $query->{where}->{'+acp'}->{call_number} = $hold_target;
2204 } elsif($hold_type eq 'T') {
2206 $query->{from}->{acp}->{acn} = {
2208 fkey => 'call_number',
2212 filter => {id => $hold_target},
2220 $query->{from}->{acp}->{acn} = {
2222 fkey => 'call_number',
2231 filter => {metarecord => $hold_target},
2239 my $res = $e->json_query($query)->[0] or return {};
2240 return {copy => $res->{id}, location => $res->{name}} if $res;
2244 # returns true if the user already has an item checked out
2245 # that could be used to fulfill the requested hold.
2246 sub hold_item_is_checked_out {
2247 my($e, $user_id, $hold_type, $hold_target) = @_;
2250 select => {acp => ['id']},
2251 from => {acp => {}},
2255 in => { # copies for circs the user has checked out
2256 select => {circ => ['target_copy']},
2260 checkin_time => undef,
2262 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2263 {stop_fines => undef}
2273 if($hold_type eq 'C') {
2275 $query->{where}->{'+acp'}->{id} = $hold_target;
2277 } elsif($hold_type eq 'V') {
2279 $query->{where}->{'+acp'}->{call_number} = $hold_target;
2281 } elsif($hold_type eq 'T') {
2283 $query->{from}->{acp}->{acn} = {
2285 fkey => 'call_number',
2289 filter => {id => $hold_target},
2297 $query->{from}->{acp}->{acn} = {
2299 fkey => 'call_number',
2308 filter => {metarecord => $hold_target},
2316 return $e->json_query($query)->[0];