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;
22 use OpenSRF::EX qw(:try);
25 use OpenSRF::Utils::Logger qw(:logger);
26 use OpenILS::Utils::CStoreEditor q/:funcs/;
27 use OpenILS::Utils::PermitHold;
28 use OpenSRF::Utils::SettingsClient;
29 use OpenILS::Const qw/:const/;
30 use OpenILS::Application::Circ::Transit;
32 my $apputils = "OpenILS::Application::AppUtils";
37 __PACKAGE__->register_method(
38 method => "create_hold",
39 api_name => "open-ils.circ.holds.create",
41 Create a new hold for an item. From a permissions perspective,
42 the login session is used as the 'requestor' of the hold.
43 The hold recipient is determined by the 'usr' setting within
46 First we verify the requestion has holds request permissions.
47 Then we verify that the recipient is allowed to make the given hold.
48 If not, we see if the requestor has "override" capabilities. If not,
49 a permission exception is returned. If permissions allow, we cycle
50 through the set of holds objects and create.
52 If the recipient does not have permission to place multiple holds
53 on a single title and said operation is attempted, a permission
58 __PACKAGE__->register_method(
59 method => "create_hold",
60 api_name => "open-ils.circ.holds.create.override",
62 If the recipient is not allowed to receive the requested hold,
63 call this method to attempt the override
64 @see open-ils.circ.holds.create
69 my( $self, $conn, $auth, @holds ) = @_;
70 my $e = new_editor(authtoken=>$auth, xact=>1);
71 return $e->event unless $e->checkauth;
73 my $override = 1 if $self->api_name =~ /override/;
75 my $holds = (ref($holds[0] eq 'ARRAY')) ? $holds[0] : [@holds];
79 for my $hold (@$holds) {
84 my $requestor = $e->requestor;
85 my $recipient = $requestor;
88 if( $requestor->id ne $hold->usr ) {
89 # Make sure the requestor is allowed to place holds for
90 # the recipient if they are not the same people
91 $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
92 $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
95 # Now make sure the recipient is allowed to receive the specified hold
97 my $porg = $recipient->home_ou;
98 my $rid = $e->requestor->id;
99 my $t = $hold->hold_type;
101 # See if a duplicate hold already exists
103 usr => $recipient->id,
105 fulfillment_time => undef,
106 target => $hold->target,
107 cancel_time => undef,
110 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
112 my $existing = $e->search_action_hold_request($sargs);
113 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
115 if( $t eq OILS_HOLD_TYPE_METARECORD )
116 { $pevt = $e->event unless $e->allowed('MR_HOLDS', $porg); }
118 if( $t eq OILS_HOLD_TYPE_TITLE )
119 { $pevt = $e->event unless $e->allowed('TITLE_HOLDS', $porg); }
121 if( $t eq OILS_HOLD_TYPE_VOLUME )
122 { $pevt = $e->event unless $e->allowed('VOLUME_HOLDS', $porg); }
124 if( $t eq OILS_HOLD_TYPE_COPY )
125 { $pevt = $e->event unless $e->allowed('COPY_HOLDS', $porg); }
127 return $pevt if $pevt;
131 for my $evt (@events) {
133 my $name = $evt->{textcode};
134 return $e->event unless $e->allowed("$name.override", $porg);
141 $hold->requestor($e->requestor->id);
142 $hold->request_lib($e->requestor->ws_ou);
143 $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
144 $hold = $e->create_action_hold_request($hold) or return $e->event;
149 $conn->respond_complete(1);
152 next if $U->is_true($_->frozen);
154 'open-ils.storage.action.hold_request.copy_targeter',
162 my( $self, $client, $login_session, @holds) = @_;
164 if(!@holds){return 0;}
165 my( $user, $evt ) = $apputils->checkses($login_session);
169 if(ref($holds[0]) eq 'ARRAY') {
171 } else { $holds = [ @holds ]; }
173 $logger->debug("Iterating over holds requests...");
175 for my $hold (@$holds) {
178 my $type = $hold->hold_type;
180 $logger->activity("User " . $user->id .
181 " creating new hold of type $type for user " . $hold->usr);
184 if($user->id ne $hold->usr) {
185 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
195 # am I allowed to place holds for this user?
196 if($hold->requestor ne $hold->usr) {
197 $perm = _check_request_holds_perm($user->id, $user->home_ou);
198 if($perm) { return $perm; }
201 # is this user allowed to have holds of this type?
202 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
203 return $perm if $perm;
205 #enforce the fact that the login is the one requesting the hold
206 $hold->requestor($user->id);
207 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
209 my $resp = $apputils->simplereq(
211 'open-ils.storage.direct.action.hold_request.create', $hold );
214 return OpenSRF::EX::ERROR ("Error creating hold");
221 # makes sure that a user has permission to place the type of requested hold
222 # returns the Perm exception if not allowed, returns undef if all is well
223 sub _check_holds_perm {
224 my($type, $user_id, $org_id) = @_;
228 if($evt = $apputils->check_perms(
229 $user_id, $org_id, "MR_HOLDS")) {
233 } elsif ($type eq "T") {
234 if($evt = $apputils->check_perms(
235 $user_id, $org_id, "TITLE_HOLDS")) {
239 } elsif($type eq "V") {
240 if($evt = $apputils->check_perms(
241 $user_id, $org_id, "VOLUME_HOLDS")) {
245 } elsif($type eq "C") {
246 if($evt = $apputils->check_perms(
247 $user_id, $org_id, "COPY_HOLDS")) {
255 # tests if the given user is allowed to place holds on another's behalf
256 sub _check_request_holds_perm {
259 if(my $evt = $apputils->check_perms(
260 $user_id, $org_id, "REQUEST_HOLDS")) {
265 __PACKAGE__->register_method(
266 method => "retrieve_holds_by_id",
267 api_name => "open-ils.circ.holds.retrieve_by_id",
269 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
270 different from the user, then the requestor must have VIEW_HOLD permissions.
274 sub retrieve_holds_by_id {
275 my($self, $client, $auth, $hold_id) = @_;
276 my $e = new_editor(authtoken=>$auth);
277 $e->checkauth or return $e->event;
278 $e->allowed('VIEW_HOLD') or return $e->event;
280 my $holds = $e->search_action_hold_request(
282 { id => $hold_id , fulfillment_time => undef },
283 { order_by => { ahr => "request_time" } }
287 flesh_hold_transits($holds);
288 flesh_hold_notices($holds, $e);
293 __PACKAGE__->register_method(
294 method => "retrieve_holds",
295 api_name => "open-ils.circ.holds.retrieve",
297 Retrieves all the holds, with hold transits attached, for the specified
298 user id. The login session is the requestor and if the requestor is
299 different from the user, then the requestor must have VIEW_HOLD permissions.
302 __PACKAGE__->register_method(
303 method => "retrieve_holds",
305 api_name => "open-ils.circ.holds.id_list.retrieve",
307 Retrieves all the hold ids for the specified
308 user id. The login session is the requestor and if the requestor is
309 different from the user, then the requestor must have VIEW_HOLD permissions.
313 my($self, $client, $login_session, $user_id) = @_;
315 my( $user, $target, $evt ) = $apputils->checkses_requestor(
316 $login_session, $user_id, 'VIEW_HOLD' );
319 my $holds = $apputils->simplereq(
321 "open-ils.cstore.direct.action.hold_request.search.atomic",
324 fulfillment_time => undef,
325 cancel_time => undef,
327 { order_by => { ahr => "request_time" } }
330 if( ! $self->api_name =~ /id_list/ ) {
331 for my $hold ( @$holds ) {
333 $apputils->simplereq(
335 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
336 { hold => $hold->id },
337 { order_by => { ahtc => 'id desc' }, limit => 1 }
343 if( $self->api_name =~ /id_list/ ) {
344 return [ map { $_->id } @$holds ];
351 __PACKAGE__->register_method(
352 method => 'user_hold_count',
353 api_name => 'open-ils.circ.hold.user.count');
355 sub user_hold_count {
356 my( $self, $conn, $auth, $userid ) = @_;
357 my $e = new_editor(authtoken=>$auth);
358 return $e->event unless $e->checkauth;
359 my $patron = $e->retrieve_actor_user($userid)
361 return $e->event unless $e->allowed('VIEW_HOLD', $patron->home_ou);
362 return __user_hold_count($self, $e, $userid);
365 sub __user_hold_count {
366 my( $self, $e, $userid ) = @_;
367 my $holds = $e->search_action_hold_request(
369 fulfillment_time => undef,
370 cancel_time => undef,
375 return scalar(@$holds);
379 __PACKAGE__->register_method(
380 method => "retrieve_holds_by_pickup_lib",
381 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
383 Retrieves all the holds, with hold transits attached, for the specified
387 __PACKAGE__->register_method(
388 method => "retrieve_holds_by_pickup_lib",
389 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
391 Retrieves all the hold ids for the specified
395 sub retrieve_holds_by_pickup_lib {
396 my($self, $client, $login_session, $ou_id) = @_;
398 #FIXME -- put an appropriate permission check here
399 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
400 # $login_session, $user_id, 'VIEW_HOLD' );
401 #return $evt if $evt;
403 my $holds = $apputils->simplereq(
405 "open-ils.cstore.direct.action.hold_request.search.atomic",
407 pickup_lib => $ou_id ,
408 fulfillment_time => undef,
411 { order_by => { ahr => "request_time" } });
414 if( ! $self->api_name =~ /id_list/ ) {
415 flesh_hold_transits($holds);
418 if( $self->api_name =~ /id_list/ ) {
419 return [ map { $_->id } @$holds ];
425 __PACKAGE__->register_method(
426 method => "cancel_hold",
427 api_name => "open-ils.circ.hold.cancel",
429 Cancels the specified hold. The login session
430 is the requestor and if the requestor is different from the usr field
431 on the hold, the requestor must have CANCEL_HOLDS permissions.
432 the hold may be either the hold object or the hold id
436 my($self, $client, $auth, $holdid) = @_;
438 my $e = new_editor(authtoken=>$auth, xact=>1);
439 return $e->event unless $e->checkauth;
441 my $hold = $e->retrieve_action_hold_request($holdid)
444 if( $e->requestor->id ne $hold->usr ) {
445 return $e->event unless $e->allowed('CANCEL_HOLDS');
448 return 1 if $hold->cancel_time;
450 # If the hold is captured, reset the copy status
451 if( $hold->capture_time and $hold->current_copy ) {
453 my $copy = $e->retrieve_asset_copy($hold->current_copy)
456 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
457 $logger->info("canceling hold $holdid whose item is on the holds shelf");
458 # $logger->info("setting copy to status 'reshelving' on hold cancel");
459 # $copy->status(OILS_COPY_STATUS_RESHELVING);
460 # $copy->editor($e->requestor->id);
461 # $copy->edit_date('now');
462 # $e->update_asset_copy($copy) or return $e->event;
464 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
467 $logger->warn("! canceling hold [$hid] that is in transit");
468 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
471 my $trans = $e->retrieve_action_transit_copy($transid);
472 # Leave the transit alive, but set the copy status to
473 # reshelving so it will be properly reshelved when it gets back home
475 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
476 $e->update_action_transit_copy($trans) or return $e->die_event;
482 $hold->cancel_time('now');
483 $e->update_action_hold_request($hold)
486 delete_hold_copy_maps($self, $e, $hold->id);
492 sub delete_hold_copy_maps {
497 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
499 $editor->delete_action_hold_copy_map($_)
500 or return $editor->event;
506 __PACKAGE__->register_method(
507 method => "update_hold",
508 api_name => "open-ils.circ.hold.update",
510 Updates the specified hold. The login session
511 is the requestor and if the requestor is different from the usr field
512 on the hold, the requestor must have UPDATE_HOLDS permissions.
516 my($self, $client, $auth, $hold) = @_;
518 my $e = new_editor(authtoken=>$auth, xact=>1);
519 return $e->die_event unless $e->checkauth;
521 my $orig_hold = $e->retrieve_action_hold_request($hold->id)
522 or return $e->die_event;
524 # don't allow the user to be changed
525 return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
527 if($hold->usr ne $e->requestor->id) {
528 # if the hold is for a different user, make sure the
529 # requestor has the appropriate permissions
530 my $usr = $e->retrieve_actor_user($hold->usr)
531 or return $e->die_event;
532 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
535 update_hold_if_frozen($self, $e, $hold, $orig_hold);
536 $e->update_action_hold_request($hold) or return $e->die_event;
542 # if the hold is frozen, this method ensures that the hold is not "targeted",
543 # that is, it clears the current_copy and prev_check_time to essentiallly
544 # reset the hold. If it is being activated, it runs the targeter in the background
545 sub update_hold_if_frozen {
546 my($self, $e, $hold, $orig_hold) = @_;
547 return if $hold->capture_time;
549 if($U->is_true($hold->frozen)) {
550 $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
551 $hold->clear_current_copy;
552 $hold->clear_prev_check_time;
555 if($U->is_true($orig_hold->frozen)) {
556 $logger->info("Running targeter on activated hold ".$hold->id);
557 $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
563 __PACKAGE__->register_method(
564 method => "retrieve_hold_status",
565 api_name => "open-ils.circ.hold.status.retrieve",
567 Calculates the current status of the hold.
568 the requestor must have VIEW_HOLD permissions if the hold is for a user
569 other than the requestor.
570 Returns -1 on error (for now)
571 Returns 1 for 'waiting for copy to become available'
572 Returns 2 for 'waiting for copy capture'
573 Returns 3 for 'in transit'
574 Returns 4 for 'arrived'
577 sub retrieve_hold_status {
578 my($self, $client, $auth, $hold_id) = @_;
580 my $e = new_editor(authtoken => $auth);
581 return $e->event unless $e->checkauth;
582 my $hold = $e->retrieve_action_hold_request($hold_id)
585 if( $e->requestor->id != $hold->usr ) {
586 return $e->event unless $e->allowed('VIEW_HOLD');
589 return _hold_status($e, $hold);
595 return 1 unless $hold->current_copy;
596 return 2 unless $hold->capture_time;
598 my $copy = $hold->current_copy;
599 unless( ref $copy ) {
600 $copy = $e->retrieve_asset_copy($hold->current_copy)
604 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
605 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
611 #sub find_local_hold {
612 # my( $class, $session, $copy, $user ) = @_;
613 # return $class->find_nearest_permitted_hold($session, $copy, $user);
617 sub fetch_open_hold_by_current_copy {
620 my $hold = $apputils->simplereq(
622 'open-ils.cstore.direct.action.hold_request.search.atomic',
623 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
624 return $hold->[0] if ref($hold);
628 sub fetch_related_holds {
631 return $apputils->simplereq(
633 'open-ils.cstore.direct.action.hold_request.search.atomic',
634 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
638 __PACKAGE__->register_method (
639 method => "hold_pull_list",
640 api_name => "open-ils.circ.hold_pull_list.retrieve",
642 Returns a list of holds that need to be "pulled"
647 __PACKAGE__->register_method (
648 method => "hold_pull_list",
649 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
651 Returns a list of hold ID's that need to be "pulled"
658 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
659 my( $reqr, $evt ) = $U->checkses($authtoken);
662 my $org = $reqr->ws_ou || $reqr->home_ou;
663 # the perm locaiton shouldn't really matter here since holds
664 # will exist all over and VIEW_HOLDS should be universal
665 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
668 if( $self->api_name =~ /id_list/ ) {
669 return $U->storagereq(
670 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
671 $org, $limit, $offset );
673 return $U->storagereq(
674 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
675 $org, $limit, $offset );
679 __PACKAGE__->register_method (
680 method => 'fetch_hold_notify',
681 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
683 Returns a list of hold notification objects based on hold id.
684 @param authtoken The loggin session key
685 @param holdid The id of the hold whose notifications we want to retrieve
686 @return An array of hold notification objects, event on error.
690 sub fetch_hold_notify {
691 my( $self, $conn, $authtoken, $holdid ) = @_;
692 my( $requestor, $evt ) = $U->checkses($authtoken);
695 ($hold, $evt) = $U->fetch_hold($holdid);
697 ($patron, $evt) = $U->fetch_user($hold->usr);
700 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
703 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
704 return $U->cstorereq(
705 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
709 __PACKAGE__->register_method (
710 method => 'create_hold_notify',
711 api_name => 'open-ils.circ.hold_notification.create',
713 Creates a new hold notification object
714 @param authtoken The login session key
715 @param notification The hold notification object to create
716 @return ID of the new object on success, Event on error
720 sub __create_hold_notify {
721 my( $self, $conn, $authtoken, $notification ) = @_;
722 my( $requestor, $evt ) = $U->checkses($authtoken);
725 ($hold, $evt) = $U->fetch_hold($notification->hold);
727 ($patron, $evt) = $U->fetch_user($hold->usr);
730 # XXX perm depth probably doesn't matter here -- should always be consortium level
731 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
734 # Set the proper notifier
735 $notification->notify_staff($requestor->id);
736 my $id = $U->storagereq(
737 'open-ils.storage.direct.action.hold_notification.create', $notification );
738 return $U->DB_UPDATE_FAILED($notification) unless $id;
739 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
744 sub create_hold_notify {
745 my( $self, $conn, $auth, $note ) = @_;
746 my $e = new_editor(authtoken=>$auth, xact=>1);
747 return $e->die_event unless $e->checkauth;
749 my $hold = $e->retrieve_action_hold_request($note->hold)
750 or return $e->die_event;
751 my $patron = $e->retrieve_actor_user($hold->usr)
752 or return $e->die_event;
754 return $e->die_event unless
755 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
757 $note->notify_staff($e->requestor->id);
758 $e->create_action_hold_notification($note) or return $e->die_event;
764 __PACKAGE__->register_method(
765 method => 'reset_hold',
766 api_name => 'open-ils.circ.hold.reset',
768 Un-captures and un-targets a hold, essentially returning
769 it to the state it was in directly after it was placed,
770 then attempts to re-target the hold
771 @param authtoken The login session key
772 @param holdid The id of the hold
778 my( $self, $conn, $auth, $holdid ) = @_;
780 my ($hold, $evt) = $U->fetch_hold($holdid);
782 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
784 $evt = _reset_hold($self, $reqr, $hold);
790 my ($self, $reqr, $hold) = @_;
792 my $e = new_editor(xact =>1, requestor => $reqr);
794 $logger->info("reseting hold ".$hold->id);
798 if( $hold->capture_time and $hold->current_copy ) {
800 my $copy = $e->retrieve_asset_copy($hold->current_copy)
803 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
804 $logger->info("setting copy to status 'reshelving' on hold retarget");
805 $copy->status(OILS_COPY_STATUS_RESHELVING);
806 $copy->editor($e->requestor->id);
807 $copy->edit_date('now');
808 $e->update_asset_copy($copy) or return $e->event;
810 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
812 # We don't want the copy to remain "in transit"
813 $copy->status(OILS_COPY_STATUS_RESHELVING);
814 $logger->warn("! reseting hold [$hid] that is in transit");
815 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
818 my $trans = $e->retrieve_action_transit_copy($transid);
820 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
821 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
822 $logger->info("Transit abort completed with result $evt");
823 return $evt unless "$evt" eq 1;
829 $hold->clear_capture_time;
830 $hold->clear_current_copy;
832 $e->update_action_hold_request($hold) or return $e->event;
836 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
842 __PACKAGE__->register_method(
843 method => 'fetch_open_title_holds',
844 api_name => 'open-ils.circ.open_holds.retrieve',
846 Returns a list ids of un-fulfilled holds for a given title id
847 @param authtoken The login session key
848 @param id the id of the item whose holds we want to retrieve
849 @param type The hold type - M, T, V, C
853 sub fetch_open_title_holds {
854 my( $self, $conn, $auth, $id, $type, $org ) = @_;
855 my $e = new_editor( authtoken => $auth );
856 return $e->event unless $e->checkauth;
859 $org ||= $e->requestor->ws_ou;
861 # return $e->search_action_hold_request(
862 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
864 # XXX make me return IDs in the future ^--
865 my $holds = $e->search_action_hold_request(
868 cancel_time => undef,
870 fulfillment_time => undef
874 flesh_hold_transits($holds);
879 sub flesh_hold_transits {
881 for my $hold ( @$holds ) {
883 $apputils->simplereq(
885 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
886 { hold => $hold->id },
887 { order_by => { ahtc => 'id desc' }, limit => 1 }
893 sub flesh_hold_notices {
894 my( $holds, $e ) = @_;
897 for my $hold (@$holds) {
898 my $notices = $e->search_action_hold_notification(
900 { hold => $hold->id },
901 { order_by => { anh => 'notify_time desc' } },
906 $hold->notify_count(scalar(@$notices));
908 my $n = $e->retrieve_action_hold_notification($$notices[0])
910 $hold->notify_time($n->notify_time);
918 __PACKAGE__->register_method(
919 method => 'fetch_captured_holds',
920 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
922 Returns a list of un-fulfilled holds for a given title id
923 @param authtoken The login session key
924 @param org The org id of the location in question
928 __PACKAGE__->register_method(
929 method => 'fetch_captured_holds',
930 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
932 Returns a list ids of un-fulfilled holds for a given title id
933 @param authtoken The login session key
934 @param org The org id of the location in question
938 sub fetch_captured_holds {
939 my( $self, $conn, $auth, $org ) = @_;
941 my $e = new_editor(authtoken => $auth);
942 return $e->event unless $e->checkauth;
943 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
945 $org ||= $e->requestor->ws_ou;
947 my $holds = $e->search_action_hold_request(
949 capture_time => { "!=" => undef },
950 current_copy => { "!=" => undef },
951 fulfillment_time => undef,
953 cancel_time => undef,
958 for my $h (@$holds) {
959 my $copy = $e->retrieve_asset_copy($h->current_copy)
962 $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
965 if( ! $self->api_name =~ /id_list/ ) {
966 flesh_hold_transits(\@res);
967 flesh_hold_notices(\@res, $e);
970 if( $self->api_name =~ /id_list/ ) {
971 return [ map { $_->id } @res ];
977 __PACKAGE__->register_method(
978 method => "check_title_hold",
979 api_name => "open-ils.circ.title_hold.is_possible",
981 Determines if a hold were to be placed by a given user,
982 whether or not said hold would have any potential copies
984 @param authtoken The login session key
985 @param params A hash of named params including:
986 patronid - the id of the hold recipient
987 titleid (brn) - the id of the title to be held
988 depth - the hold range depth (defaults to 0)
991 sub check_title_hold {
992 my( $self, $client, $authtoken, $params ) = @_;
994 my %params = %$params;
995 my $titleid = $params{titleid} ||"";
996 my $volid = $params{volume_id};
997 my $copyid = $params{copy_id};
998 my $mrid = $params{mrid} ||"";
999 my $depth = $params{depth} || 0;
1000 my $pickup_lib = $params{pickup_lib};
1001 my $hold_type = $params{hold_type} || 'T';
1002 my $selection_ou = $params{selection_ou} || $pickup_lib;
1004 my $e = new_editor(authtoken=>$authtoken);
1005 return $e->event unless $e->checkauth;
1006 my $patron = $e->retrieve_actor_user($params{patronid})
1007 or return $e->event;
1009 if( $e->requestor->id ne $patron->id ) {
1010 return $e->event unless
1011 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1014 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1016 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1017 or return $e->event;
1019 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1020 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1022 if(defined $soft_boundary and $$params{depth} < $soft_boundary) {
1023 # work up the tree and as soon as we find a potential copy, use that depth
1024 # also, make sure we don't go past the hard boundary if it exists
1026 # our min boundary is the greater of user-specified boundary or hard boundary
1027 my $min_depth = (defined $hard_boundary and $hard_boundary > $$params{depth}) ?
1028 $hard_boundary : $$params{depth};
1030 my $depth = $soft_boundary;
1031 while($depth >= $min_depth) {
1032 $logger->info("performing hold possibility check with soft boundary $depth");
1033 return {success => 1, depth => $depth}
1034 if do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1037 return {success => 0};
1039 } elsif(defined $hard_boundary and $$params{depth} < $hard_boundary) {
1040 # there is no soft boundary, enforce the hard boundary if it exists
1041 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1042 if(do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params)) {
1043 return {success => 1, depth => $hard_boundary}
1045 return {success => 0};
1049 # no boundaries defined, fall back to user specifed boundary or no boundary
1050 $logger->info("performing hold possibility check with no boundary");
1051 if(do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params)) {
1052 return {success => 1, depth => $hard_boundary};
1054 return {success => 0};
1059 sub do_possibility_checks {
1060 my($e, $patron, $request_lib, $depth, %params) = @_;
1062 my $titleid = $params{titleid} ||"";
1063 my $volid = $params{volume_id};
1064 my $copyid = $params{copy_id};
1065 my $mrid = $params{mrid} ||"";
1066 my $pickup_lib = $params{pickup_lib};
1067 my $hold_type = $params{hold_type} || 'T';
1068 my $selection_ou = $params{selection_ou} || $pickup_lib;
1075 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1077 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1078 $volume = $e->retrieve_asset_call_number($copy->call_number)
1079 or return $e->event;
1080 $title = $e->retrieve_biblio_record_entry($volume->record)
1081 or return $e->event;
1082 return verify_copy_for_hold(
1083 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1085 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1087 $volume = $e->retrieve_asset_call_number($volid)
1088 or return $e->event;
1089 $title = $e->retrieve_biblio_record_entry($volume->record)
1090 or return $e->event;
1092 return _check_volume_hold_is_possible(
1093 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1095 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1097 return _check_title_hold_is_possible(
1098 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1100 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1102 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1103 my @recs = map { $_->source } @$maps;
1104 for my $rec (@recs) {
1105 return 1 if (_check_title_hold_is_possible(
1106 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou));
1114 sub _check_metarecord_hold_is_possible {
1115 my( $mrid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1117 my $e = new_editor();
1119 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given metarecord
1120 my $copies = $e->json_query(
1122 select => { acp => ['id', 'circ_lib'] },
1127 fkey => 'call_number',
1132 filter => { metarecord => $mrid }
1136 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1137 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1141 '+acp' => { circulate => 't', deleted => 'f', holdable => 't' }
1146 return $e->event unless defined $copies;
1147 $logger->info("metarecord possible found ".scalar(@$copies)." potential copies");
1148 return 0 unless @$copies;
1150 # -----------------------------------------------------------------------
1151 # sort the copies into buckets based on their circ_lib proximity to
1152 # the patron's home_ou.
1153 # -----------------------------------------------------------------------
1155 my $home_org = $patron->home_ou;
1156 my $req_org = $request_lib->id;
1159 ($prox_cache{$home_org}) ?
1160 $prox_cache{$home_org} :
1161 $prox_cache{$home_org} = $e->search_actor_org_unit_proximity({from_org => $home_org});
1164 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1165 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1167 my @keys = sort { $a <=> $b } keys %buckets;
1170 if( $home_org ne $req_org ) {
1171 # -----------------------------------------------------------------------
1172 # shove the copies close to the request_lib into the primary buckets
1173 # directly before the farthest away copies. That way, they are not
1174 # given priority, but they are checked before the farthest copies.
1175 # -----------------------------------------------------------------------
1177 ($prox_cache{$req_org}) ?
1178 $prox_cache{$req_org} :
1179 $prox_cache{$req_org} = $e->search_actor_org_unit_proximity({from_org => $req_org});
1182 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1183 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1185 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1186 my $new_key = $highest_key - 0.5; # right before the farthest prox
1187 my @keys2 = sort { $a <=> $b } keys %buckets2;
1188 for my $key (@keys2) {
1189 last if $key >= $highest_key;
1190 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1194 @keys = sort { $a <=> $b } keys %buckets;
1197 for my $key (@keys) {
1198 my @cps = @{$buckets{$key}};
1200 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1202 for my $copyid (@cps) {
1204 next if $seen{$copyid};
1205 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1206 my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1207 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1209 my $vol = $e->retrieve_asset_call_number(
1210 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1212 return 1 if verify_copy_for_hold(
1213 $patron, $requestor, $vol->record, $copy, $pickup_lib, $request_lib );
1221 sub create_ranged_org_filter {
1222 my($e, $selection_ou, $depth) = @_;
1224 # find the orgs from which this hold may be fulfilled,
1225 # based on the selection_ou and depth
1227 my $top_org = $e->search_actor_org_unit([
1228 {parent_ou => undef},
1229 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1232 return () if $depth == $top_org->ou_type->depth;
1234 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1235 %org_filter = (circ_lib => []);
1236 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1238 $logger->info("hold org filter at depth $depth and selection_ou ".
1239 "$selection_ou created list of @{$org_filter{circ_lib}}");
1245 sub _check_title_hold_is_possible {
1246 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1248 my $e = new_editor();
1249 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1251 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1252 my $copies = $e->json_query(
1254 select => { acp => ['id', 'circ_lib'] },
1259 fkey => 'call_number',
1263 filter => { id => $titleid },
1268 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1269 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1273 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1278 return $e->event unless defined $copies;
1279 $logger->info("title possible found ".scalar(@$copies)." potential copies");
1280 return 0 unless @$copies;
1282 # -----------------------------------------------------------------------
1283 # sort the copies into buckets based on their circ_lib proximity to
1284 # the patron's home_ou.
1285 # -----------------------------------------------------------------------
1287 my $home_org = $patron->home_ou;
1288 my $req_org = $request_lib->id;
1291 ($prox_cache{$home_org}) ?
1292 $prox_cache{$home_org} :
1293 $prox_cache{$home_org} = $e->search_actor_org_unit_proximity({from_org => $home_org});
1296 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1297 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1299 my @keys = sort { $a <=> $b } keys %buckets;
1302 if( $home_org ne $req_org ) {
1303 # -----------------------------------------------------------------------
1304 # shove the copies close to the request_lib into the primary buckets
1305 # directly before the farthest away copies. That way, they are not
1306 # given priority, but they are checked before the farthest copies.
1307 # -----------------------------------------------------------------------
1309 ($prox_cache{$req_org}) ?
1310 $prox_cache{$req_org} :
1311 $prox_cache{$req_org} = $e->search_actor_org_unit_proximity({from_org => $req_org});
1314 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1315 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1317 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1318 my $new_key = $highest_key - 0.5; # right before the farthest prox
1319 my @keys2 = sort { $a <=> $b } keys %buckets2;
1320 for my $key (@keys2) {
1321 last if $key >= $highest_key;
1322 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1326 @keys = sort { $a <=> $b } keys %buckets;
1330 for my $key (@keys) {
1331 my @cps = @{$buckets{$key}};
1333 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1335 for my $copyid (@cps) {
1337 next if $seen{$copyid};
1338 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1339 my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1340 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1342 unless($title) { # grab the title if we don't already have it
1343 my $vol = $e->retrieve_asset_call_number(
1344 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1345 $title = $vol->record;
1348 return 1 if verify_copy_for_hold(
1349 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1358 sub _check_volume_hold_is_possible {
1359 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1360 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1361 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1362 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1363 for my $copy ( @$copies ) {
1364 return 1 if verify_copy_for_hold(
1365 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1372 sub verify_copy_for_hold {
1373 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1374 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1375 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1376 { patron => $patron,
1377 requestor => $requestor,
1380 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1381 pickup_lib => $pickup_lib,
1382 request_lib => $request_lib,
1391 sub find_nearest_permitted_hold {
1394 my $editor = shift; # CStoreEditor object
1395 my $copy = shift; # copy to target
1396 my $user = shift; # staff
1397 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1398 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1400 my $bc = $copy->barcode;
1402 # find any existing holds that already target this copy
1403 my $old_holds = $editor->search_action_hold_request(
1404 { current_copy => $copy->id,
1405 cancel_time => undef,
1406 capture_time => undef
1410 # hold->type "R" means we need this copy
1411 for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1414 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1416 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1417 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1419 # search for what should be the best holds for this copy to fulfill
1420 my $best_holds = $U->storagereq(
1421 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1422 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1424 unless(@$best_holds) {
1426 if( my $hold = $$old_holds[0] ) {
1427 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1431 $logger->info("circulator: no suitable holds found for copy $bc");
1432 return (undef, $evt);
1438 # for each potential hold, we have to run the permit script
1439 # to make sure the hold is actually permitted.
1440 for my $holdid (@$best_holds) {
1441 next unless $holdid;
1442 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1444 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1445 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1446 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1448 # see if this hold is permitted
1449 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1450 { patron_id => $hold->usr,
1453 pickup_lib => $hold->pickup_lib,
1454 request_lib => $rlib,
1465 unless( $best_hold ) { # no "good" permitted holds were found
1466 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1467 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1472 $logger->info("circulator: no suitable holds found for copy $bc");
1473 return (undef, $evt);
1476 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1478 # indicate a permitted hold was found
1479 return $best_hold if $check_only;
1481 # we've found a permitted hold. we need to "grab" the copy
1482 # to prevent re-targeted holds (next part) from re-grabbing the copy
1483 $best_hold->current_copy($copy->id);
1484 $editor->update_action_hold_request($best_hold)
1485 or return (undef, $editor->event);
1490 # re-target any other holds that already target this copy
1491 for my $old_hold (@$old_holds) {
1492 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1493 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1494 $old_hold->id." after a better hold [".$best_hold->id."] was found");
1495 $old_hold->clear_current_copy;
1496 $old_hold->clear_prev_check_time;
1497 $editor->update_action_hold_request($old_hold)
1498 or return (undef, $editor->event);
1502 return ($best_hold, undef, $retarget);
1510 __PACKAGE__->register_method(
1511 method => 'all_rec_holds',
1512 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1516 my( $self, $conn, $auth, $title_id, $args ) = @_;
1518 my $e = new_editor(authtoken=>$auth);
1519 $e->checkauth or return $e->event;
1520 $e->allowed('VIEW_HOLD') or return $e->event;
1522 $args ||= { fulfillment_time => undef };
1523 $args->{cancel_time} = undef;
1525 my $resp = { volume_holds => [], copy_holds => [] };
1527 $resp->{title_holds} = $e->search_action_hold_request(
1529 hold_type => OILS_HOLD_TYPE_TITLE,
1530 target => $title_id,
1534 my $vols = $e->search_asset_call_number(
1535 { record => $title_id, deleted => 'f' }, {idlist=>1});
1537 return $resp unless @$vols;
1539 $resp->{volume_holds} = $e->search_action_hold_request(
1541 hold_type => OILS_HOLD_TYPE_VOLUME,
1546 my $copies = $e->search_asset_copy(
1547 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1549 return $resp unless @$copies;
1551 $resp->{copy_holds} = $e->search_action_hold_request(
1553 hold_type => OILS_HOLD_TYPE_COPY,
1565 __PACKAGE__->register_method(
1566 method => 'uber_hold',
1568 api_name => 'open-ils.circ.hold.details.retrieve'
1572 my($self, $client, $auth, $hold_id) = @_;
1573 my $e = new_editor(authtoken=>$auth);
1574 $e->checkauth or return $e->event;
1575 $e->allowed('VIEW_HOLD') or return $e->event;
1579 my $hold = $e->retrieve_action_hold_request(
1584 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1587 ) or return $e->event;
1589 my $user = $hold->usr;
1590 $hold->usr($user->id);
1592 my $card = $e->retrieve_actor_card($user->card)
1593 or return $e->event;
1595 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1597 flesh_hold_notices([$hold], $e);
1598 flesh_hold_transits([$hold]);
1605 status => _hold_status($e, $hold),
1606 patron_first => $user->first_given_name,
1607 patron_last => $user->family_name,
1608 patron_barcode => $card->barcode,
1614 # -----------------------------------------------------
1615 # Returns the MVR object that represents what the
1617 # -----------------------------------------------------
1619 my( $e, $hold ) = @_;
1625 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1626 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1627 or return $e->event;
1628 $tid = $mr->master_record;
1630 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1631 $tid = $hold->target;
1633 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1634 $volume = $e->retrieve_asset_call_number($hold->target)
1635 or return $e->event;
1636 $tid = $volume->record;
1638 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1639 $copy = $e->retrieve_asset_copy($hold->target)
1640 or return $e->event;
1641 $volume = $e->retrieve_asset_call_number($copy->call_number)
1642 or return $e->event;
1643 $tid = $volume->record;
1646 if(!$copy and ref $hold->current_copy ) {
1647 $copy = $hold->current_copy;
1648 $hold->current_copy($copy->id);
1651 if(!$volume and $copy) {
1652 $volume = $e->retrieve_asset_call_number($copy->call_number);
1655 my $title = $e->retrieve_biblio_record_entry($tid);
1656 return ( $U->record_to_mvr($title), $volume, $copy );