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);
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 if($hold->usr ne $e->requestor->id) {
522 # if the hold is for a different user, make sure the
523 # requestor has the appropriate permissions
524 my $usr = $e->retrieve_actor_user($hold->usr)
525 or return $e->die_event;
526 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
529 my $evt = update_hold_if_frozen($self, $e, $hold);
532 $e->update_action_hold_request($hold)
533 or return $e->die_event;
540 # if the hold is frozen, this method ensures that the hold is not "targeted",
541 # that is, it clears the current_copy and prev_check_time to essentiallly
543 sub update_hold_if_frozen {
544 my($self, $e, $hold) = @_;
545 return undef if $hold->capture_time;
546 if($hold->frozen and ($hold->current_copy or $hold->prev_check_time)) {
547 $logger->info("clearing current_copy and check_time for frozen hold");
548 $hold->clear_current_copy;
549 $hold->clear_prev_check_time;
550 $e->update_action_hold_request($hold) or return $e->die_event;
556 __PACKAGE__->register_method(
557 method => "retrieve_hold_status",
558 api_name => "open-ils.circ.hold.status.retrieve",
560 Calculates the current status of the hold.
561 the requestor must have VIEW_HOLD permissions if the hold is for a user
562 other than the requestor.
563 Returns -1 on error (for now)
564 Returns 1 for 'waiting for copy to become available'
565 Returns 2 for 'waiting for copy capture'
566 Returns 3 for 'in transit'
567 Returns 4 for 'arrived'
570 sub retrieve_hold_status {
571 my($self, $client, $auth, $hold_id) = @_;
573 my $e = new_editor(authtoken => $auth);
574 return $e->event unless $e->checkauth;
575 my $hold = $e->retrieve_action_hold_request($hold_id)
578 if( $e->requestor->id != $hold->usr ) {
579 return $e->event unless $e->allowed('VIEW_HOLD');
582 return _hold_status($e, $hold);
588 return 1 unless $hold->current_copy;
589 return 2 unless $hold->capture_time;
591 my $copy = $hold->current_copy;
592 unless( ref $copy ) {
593 $copy = $e->retrieve_asset_copy($hold->current_copy)
597 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
598 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
604 #sub find_local_hold {
605 # my( $class, $session, $copy, $user ) = @_;
606 # return $class->find_nearest_permitted_hold($session, $copy, $user);
610 sub fetch_open_hold_by_current_copy {
613 my $hold = $apputils->simplereq(
615 'open-ils.cstore.direct.action.hold_request.search.atomic',
616 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
617 return $hold->[0] if ref($hold);
621 sub fetch_related_holds {
624 return $apputils->simplereq(
626 'open-ils.cstore.direct.action.hold_request.search.atomic',
627 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
631 __PACKAGE__->register_method (
632 method => "hold_pull_list",
633 api_name => "open-ils.circ.hold_pull_list.retrieve",
635 Returns a list of holds that need to be "pulled"
640 __PACKAGE__->register_method (
641 method => "hold_pull_list",
642 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
644 Returns a list of hold ID's that need to be "pulled"
651 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
652 my( $reqr, $evt ) = $U->checkses($authtoken);
655 my $org = $reqr->ws_ou || $reqr->home_ou;
656 # the perm locaiton shouldn't really matter here since holds
657 # will exist all over and VIEW_HOLDS should be universal
658 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
661 if( $self->api_name =~ /id_list/ ) {
662 return $U->storagereq(
663 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
664 $org, $limit, $offset );
666 return $U->storagereq(
667 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
668 $org, $limit, $offset );
672 __PACKAGE__->register_method (
673 method => 'fetch_hold_notify',
674 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
676 Returns a list of hold notification objects based on hold id.
677 @param authtoken The loggin session key
678 @param holdid The id of the hold whose notifications we want to retrieve
679 @return An array of hold notification objects, event on error.
683 sub fetch_hold_notify {
684 my( $self, $conn, $authtoken, $holdid ) = @_;
685 my( $requestor, $evt ) = $U->checkses($authtoken);
688 ($hold, $evt) = $U->fetch_hold($holdid);
690 ($patron, $evt) = $U->fetch_user($hold->usr);
693 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
696 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
697 return $U->cstorereq(
698 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
702 __PACKAGE__->register_method (
703 method => 'create_hold_notify',
704 api_name => 'open-ils.circ.hold_notification.create',
706 Creates a new hold notification object
707 @param authtoken The login session key
708 @param notification The hold notification object to create
709 @return ID of the new object on success, Event on error
713 sub __create_hold_notify {
714 my( $self, $conn, $authtoken, $notification ) = @_;
715 my( $requestor, $evt ) = $U->checkses($authtoken);
718 ($hold, $evt) = $U->fetch_hold($notification->hold);
720 ($patron, $evt) = $U->fetch_user($hold->usr);
723 # XXX perm depth probably doesn't matter here -- should always be consortium level
724 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
727 # Set the proper notifier
728 $notification->notify_staff($requestor->id);
729 my $id = $U->storagereq(
730 'open-ils.storage.direct.action.hold_notification.create', $notification );
731 return $U->DB_UPDATE_FAILED($notification) unless $id;
732 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
737 sub create_hold_notify {
738 my( $self, $conn, $auth, $note ) = @_;
739 my $e = new_editor(authtoken=>$auth, xact=>1);
740 return $e->die_event unless $e->checkauth;
742 my $hold = $e->retrieve_action_hold_request($note->hold)
743 or return $e->die_event;
744 my $patron = $e->retrieve_actor_user($hold->usr)
745 or return $e->die_event;
747 return $e->die_event unless
748 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
750 $note->notify_staff($e->requestor->id);
751 $e->create_action_hold_notification($note) or return $e->die_event;
757 __PACKAGE__->register_method(
758 method => 'reset_hold',
759 api_name => 'open-ils.circ.hold.reset',
761 Un-captures and un-targets a hold, essentially returning
762 it to the state it was in directly after it was placed,
763 then attempts to re-target the hold
764 @param authtoken The login session key
765 @param holdid The id of the hold
771 my( $self, $conn, $auth, $holdid ) = @_;
773 my ($hold, $evt) = $U->fetch_hold($holdid);
775 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
777 $evt = _reset_hold($self, $reqr, $hold);
783 my ($self, $reqr, $hold) = @_;
785 my $e = new_editor(xact =>1, requestor => $reqr);
787 $logger->info("reseting hold ".$hold->id);
791 if( $hold->capture_time and $hold->current_copy ) {
793 my $copy = $e->retrieve_asset_copy($hold->current_copy)
796 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
797 $logger->info("setting copy to status 'reshelving' on hold retarget");
798 $copy->status(OILS_COPY_STATUS_RESHELVING);
799 $copy->editor($e->requestor->id);
800 $copy->edit_date('now');
801 $e->update_asset_copy($copy) or return $e->event;
803 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
805 # We don't want the copy to remain "in transit"
806 $copy->status(OILS_COPY_STATUS_RESHELVING);
807 $logger->warn("! reseting hold [$hid] that is in transit");
808 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
811 my $trans = $e->retrieve_action_transit_copy($transid);
813 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
814 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
815 $logger->info("Transit abort completed with result $evt");
816 return $evt unless "$evt" eq 1;
822 $hold->clear_capture_time;
823 $hold->clear_current_copy;
825 $e->update_action_hold_request($hold) or return $e->event;
829 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
835 __PACKAGE__->register_method(
836 method => 'fetch_open_title_holds',
837 api_name => 'open-ils.circ.open_holds.retrieve',
839 Returns a list ids of un-fulfilled holds for a given title id
840 @param authtoken The login session key
841 @param id the id of the item whose holds we want to retrieve
842 @param type The hold type - M, T, V, C
846 sub fetch_open_title_holds {
847 my( $self, $conn, $auth, $id, $type, $org ) = @_;
848 my $e = new_editor( authtoken => $auth );
849 return $e->event unless $e->checkauth;
852 $org ||= $e->requestor->ws_ou;
854 # return $e->search_action_hold_request(
855 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
857 # XXX make me return IDs in the future ^--
858 my $holds = $e->search_action_hold_request(
861 cancel_time => undef,
863 fulfillment_time => undef
867 flesh_hold_transits($holds);
872 sub flesh_hold_transits {
874 for my $hold ( @$holds ) {
876 $apputils->simplereq(
878 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
879 { hold => $hold->id },
880 { order_by => { ahtc => 'id desc' }, limit => 1 }
886 sub flesh_hold_notices {
887 my( $holds, $e ) = @_;
890 for my $hold (@$holds) {
891 my $notices = $e->search_action_hold_notification(
893 { hold => $hold->id },
894 { order_by => { anh => 'notify_time desc' } },
899 $hold->notify_count(scalar(@$notices));
901 my $n = $e->retrieve_action_hold_notification($$notices[0])
903 $hold->notify_time($n->notify_time);
911 __PACKAGE__->register_method(
912 method => 'fetch_captured_holds',
913 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
915 Returns a list of un-fulfilled holds for a given title id
916 @param authtoken The login session key
917 @param org The org id of the location in question
921 __PACKAGE__->register_method(
922 method => 'fetch_captured_holds',
923 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
925 Returns a list ids of un-fulfilled holds for a given title id
926 @param authtoken The login session key
927 @param org The org id of the location in question
931 sub fetch_captured_holds {
932 my( $self, $conn, $auth, $org ) = @_;
934 my $e = new_editor(authtoken => $auth);
935 return $e->event unless $e->checkauth;
936 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
938 $org ||= $e->requestor->ws_ou;
940 my $holds = $e->search_action_hold_request(
942 capture_time => { "!=" => undef },
943 current_copy => { "!=" => undef },
944 fulfillment_time => undef,
946 cancel_time => undef,
951 for my $h (@$holds) {
952 my $copy = $e->retrieve_asset_copy($h->current_copy)
955 $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
958 if( ! $self->api_name =~ /id_list/ ) {
959 flesh_hold_transits(\@res);
960 flesh_hold_notices(\@res, $e);
963 if( $self->api_name =~ /id_list/ ) {
964 return [ map { $_->id } @res ];
970 __PACKAGE__->register_method(
971 method => "check_title_hold",
972 api_name => "open-ils.circ.title_hold.is_possible",
974 Determines if a hold were to be placed by a given user,
975 whether or not said hold would have any potential copies
977 @param authtoken The login session key
978 @param params A hash of named params including:
979 patronid - the id of the hold recipient
980 titleid (brn) - the id of the title to be held
981 depth - the hold range depth (defaults to 0)
984 sub check_title_hold {
985 my( $self, $client, $authtoken, $params ) = @_;
987 my %params = %$params;
988 my $titleid = $params{titleid} ||"";
989 my $volid = $params{volume_id};
990 my $copyid = $params{copy_id};
991 my $mrid = $params{mrid} ||"";
992 my $depth = $params{depth} || 0;
993 my $pickup_lib = $params{pickup_lib};
994 my $hold_type = $params{hold_type} || 'T';
995 my $selection_ou = $params{selection_ou} || $pickup_lib;
997 my $e = new_editor(authtoken=>$authtoken);
998 return $e->event unless $e->checkauth;
999 my $patron = $e->retrieve_actor_user($params{patronid})
1000 or return $e->event;
1002 if( $e->requestor->id ne $patron->id ) {
1003 return $e->event unless
1004 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1007 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1009 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1010 or return $e->event;
1012 my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
1013 my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
1015 if(defined $soft_boundary and $$params{depth} < $soft_boundary) {
1016 # work up the tree and as soon as we find a potential copy, use that depth
1017 # also, make sure we don't go past the hard boundary if it exists
1019 # our min boundary is the greater of user-specified boundary or hard boundary
1020 my $min_depth = (defined $hard_boundary and $hard_boundary > $$params{depth}) ?
1021 $hard_boundary : $$params{depth};
1023 my $depth = $soft_boundary;
1024 while($depth >= $min_depth) {
1025 $logger->info("performing hold possibility check with soft boundary $depth");
1026 return {success => 1, depth => $depth}
1027 if do_possibility_checks($e, $patron, $request_lib, $depth, %params);
1030 return {success => 0};
1032 } elsif(defined $hard_boundary and $$params{depth} < $hard_boundary) {
1033 # there is no soft boundary, enforce the hard boundary if it exists
1034 $logger->info("performing hold possibility check with hard boundary $hard_boundary");
1035 if(do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params)) {
1036 return {success => 1, depth => $hard_boundary}
1038 return {success => 0};
1042 # no boundaries defined, fall back to user specifed boundary or no boundary
1043 $logger->info("performing hold possibility check with no boundary");
1044 if(do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params)) {
1045 return {success => 1, depth => $hard_boundary};
1047 return {success => 0};
1052 sub do_possibility_checks {
1053 my($e, $patron, $request_lib, $depth, %params) = @_;
1055 my $titleid = $params{titleid} ||"";
1056 my $volid = $params{volume_id};
1057 my $copyid = $params{copy_id};
1058 my $mrid = $params{mrid} ||"";
1059 my $pickup_lib = $params{pickup_lib};
1060 my $hold_type = $params{hold_type} || 'T';
1061 my $selection_ou = $params{selection_ou} || $pickup_lib;
1068 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1070 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1071 $volume = $e->retrieve_asset_call_number($copy->call_number)
1072 or return $e->event;
1073 $title = $e->retrieve_biblio_record_entry($volume->record)
1074 or return $e->event;
1075 return verify_copy_for_hold(
1076 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1078 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1080 $volume = $e->retrieve_asset_call_number($volid)
1081 or return $e->event;
1082 $title = $e->retrieve_biblio_record_entry($volume->record)
1083 or return $e->event;
1085 return _check_volume_hold_is_possible(
1086 $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1088 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1090 return _check_title_hold_is_possible(
1091 $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou);
1093 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1095 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1096 my @recs = map { $_->source } @$maps;
1097 for my $rec (@recs) {
1098 return 1 if (_check_title_hold_is_possible(
1099 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou));
1107 sub _check_metarecord_hold_is_possible {
1108 my( $mrid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1110 my $e = new_editor();
1112 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given metarecord
1113 my $copies = $e->json_query(
1115 select => { acp => ['id', 'circ_lib'] },
1120 fkey => 'call_number',
1125 filter => { metarecord => $mrid }
1129 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1130 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1134 '+acp' => { circulate => 't', deleted => 'f', holdable => 't' }
1139 return $e->event unless defined $copies;
1140 $logger->info("metarecord possible found ".scalar(@$copies)." potential copies");
1141 return 0 unless @$copies;
1143 # -----------------------------------------------------------------------
1144 # sort the copies into buckets based on their circ_lib proximity to
1145 # the patron's home_ou.
1146 # -----------------------------------------------------------------------
1148 my $home_org = $patron->home_ou;
1149 my $req_org = $request_lib->id;
1152 ($prox_cache{$home_org}) ?
1153 $prox_cache{$home_org} :
1154 $prox_cache{$home_org} = $e->search_actor_org_unit_proximity({from_org => $home_org});
1157 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1158 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1160 my @keys = sort { $a <=> $b } keys %buckets;
1163 if( $home_org ne $req_org ) {
1164 # -----------------------------------------------------------------------
1165 # shove the copies close to the request_lib into the primary buckets
1166 # directly before the farthest away copies. That way, they are not
1167 # given priority, but they are checked before the farthest copies.
1168 # -----------------------------------------------------------------------
1170 ($prox_cache{$req_org}) ?
1171 $prox_cache{$req_org} :
1172 $prox_cache{$req_org} = $e->search_actor_org_unit_proximity({from_org => $req_org});
1175 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1176 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1178 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1179 my $new_key = $highest_key - 0.5; # right before the farthest prox
1180 my @keys2 = sort { $a <=> $b } keys %buckets2;
1181 for my $key (@keys2) {
1182 last if $key >= $highest_key;
1183 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1187 @keys = sort { $a <=> $b } keys %buckets;
1190 for my $key (@keys) {
1191 my @cps = @{$buckets{$key}};
1193 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1195 for my $copyid (@cps) {
1197 next if $seen{$copyid};
1198 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1199 my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1200 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1202 my $vol = $e->retrieve_asset_call_number(
1203 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1205 return 1 if verify_copy_for_hold(
1206 $patron, $requestor, $vol->record, $copy, $pickup_lib, $request_lib );
1214 sub create_ranged_org_filter {
1215 my($e, $selection_ou, $depth) = @_;
1217 # find the orgs from which this hold may be fulfilled,
1218 # based on the selection_ou and depth
1220 my $top_org = $e->search_actor_org_unit([
1221 {parent_ou => undef},
1222 {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
1225 return () if $depth == $top_org->ou_type->depth;
1227 my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
1228 %org_filter = (circ_lib => []);
1229 push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
1231 $logger->info("hold org filter at depth $depth and selection_ou ".
1232 "$selection_ou created list of @{$org_filter{circ_lib}}");
1238 sub _check_title_hold_is_possible {
1239 my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1241 my $e = new_editor();
1242 my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
1244 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1245 my $copies = $e->json_query(
1247 select => { acp => ['id', 'circ_lib'] },
1252 fkey => 'call_number',
1256 filter => { id => $titleid },
1261 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1262 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1266 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
1271 return $e->event unless defined $copies;
1272 $logger->info("title possible found ".scalar(@$copies)." potential copies");
1273 return 0 unless @$copies;
1275 # -----------------------------------------------------------------------
1276 # sort the copies into buckets based on their circ_lib proximity to
1277 # the patron's home_ou.
1278 # -----------------------------------------------------------------------
1280 my $home_org = $patron->home_ou;
1281 my $req_org = $request_lib->id;
1284 ($prox_cache{$home_org}) ?
1285 $prox_cache{$home_org} :
1286 $prox_cache{$home_org} = $e->search_actor_org_unit_proximity({from_org => $home_org});
1289 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1290 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1292 my @keys = sort { $a <=> $b } keys %buckets;
1295 if( $home_org ne $req_org ) {
1296 # -----------------------------------------------------------------------
1297 # shove the copies close to the request_lib into the primary buckets
1298 # directly before the farthest away copies. That way, they are not
1299 # given priority, but they are checked before the farthest copies.
1300 # -----------------------------------------------------------------------
1302 ($prox_cache{$req_org}) ?
1303 $prox_cache{$req_org} :
1304 $prox_cache{$req_org} = $e->search_actor_org_unit_proximity({from_org => $req_org});
1307 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1308 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1310 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1311 my $new_key = $highest_key - 0.5; # right before the farthest prox
1312 my @keys2 = sort { $a <=> $b } keys %buckets2;
1313 for my $key (@keys2) {
1314 last if $key >= $highest_key;
1315 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1319 @keys = sort { $a <=> $b } keys %buckets;
1323 for my $key (@keys) {
1324 my @cps = @{$buckets{$key}};
1326 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1328 for my $copyid (@cps) {
1330 next if $seen{$copyid};
1331 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1332 my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1333 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1335 unless($title) { # grab the title if we don't already have it
1336 my $vol = $e->retrieve_asset_call_number(
1337 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1338 $title = $vol->record;
1341 return 1 if verify_copy_for_hold(
1342 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1351 sub _check_volume_hold_is_possible {
1352 my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
1353 my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
1354 my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
1355 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1356 for my $copy ( @$copies ) {
1357 return 1 if verify_copy_for_hold(
1358 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1365 sub verify_copy_for_hold {
1366 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1367 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1368 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1369 { patron => $patron,
1370 requestor => $requestor,
1373 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1374 pickup_lib => $pickup_lib,
1375 request_lib => $request_lib,
1384 sub find_nearest_permitted_hold {
1387 my $editor = shift; # CStoreEditor object
1388 my $copy = shift; # copy to target
1389 my $user = shift; # staff
1390 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1391 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1393 my $bc = $copy->barcode;
1395 # find any existing holds that already target this copy
1396 my $old_holds = $editor->search_action_hold_request(
1397 { current_copy => $copy->id,
1398 cancel_time => undef,
1399 capture_time => undef
1403 # hold->type "R" means we need this copy
1404 for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1407 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
1409 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1410 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1412 # search for what should be the best holds for this copy to fulfill
1413 my $best_holds = $U->storagereq(
1414 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1415 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1417 unless(@$best_holds) {
1419 if( my $hold = $$old_holds[0] ) {
1420 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1424 $logger->info("circulator: no suitable holds found for copy $bc");
1425 return (undef, $evt);
1431 # for each potential hold, we have to run the permit script
1432 # to make sure the hold is actually permitted.
1433 for my $holdid (@$best_holds) {
1434 next unless $holdid;
1435 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1437 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1438 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1439 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1441 # see if this hold is permitted
1442 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1443 { patron_id => $hold->usr,
1446 pickup_lib => $hold->pickup_lib,
1447 request_lib => $rlib,
1458 unless( $best_hold ) { # no "good" permitted holds were found
1459 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1460 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1465 $logger->info("circulator: no suitable holds found for copy $bc");
1466 return (undef, $evt);
1469 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1471 # indicate a permitted hold was found
1472 return $best_hold if $check_only;
1474 # we've found a permitted hold. we need to "grab" the copy
1475 # to prevent re-targeted holds (next part) from re-grabbing the copy
1476 $best_hold->current_copy($copy->id);
1477 $editor->update_action_hold_request($best_hold)
1478 or return (undef, $editor->event);
1483 # re-target any other holds that already target this copy
1484 for my $old_hold (@$old_holds) {
1485 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1486 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1487 $old_hold->id." after a better hold [".$best_hold->id."] was found");
1488 $old_hold->clear_current_copy;
1489 $old_hold->clear_prev_check_time;
1490 $editor->update_action_hold_request($old_hold)
1491 or return (undef, $editor->event);
1495 return ($best_hold, undef, $retarget);
1503 __PACKAGE__->register_method(
1504 method => 'all_rec_holds',
1505 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1509 my( $self, $conn, $auth, $title_id, $args ) = @_;
1511 my $e = new_editor(authtoken=>$auth);
1512 $e->checkauth or return $e->event;
1513 $e->allowed('VIEW_HOLD') or return $e->event;
1515 $args ||= { fulfillment_time => undef };
1516 $args->{cancel_time} = undef;
1518 my $resp = { volume_holds => [], copy_holds => [] };
1520 $resp->{title_holds} = $e->search_action_hold_request(
1522 hold_type => OILS_HOLD_TYPE_TITLE,
1523 target => $title_id,
1527 my $vols = $e->search_asset_call_number(
1528 { record => $title_id, deleted => 'f' }, {idlist=>1});
1530 return $resp unless @$vols;
1532 $resp->{volume_holds} = $e->search_action_hold_request(
1534 hold_type => OILS_HOLD_TYPE_VOLUME,
1539 my $copies = $e->search_asset_copy(
1540 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1542 return $resp unless @$copies;
1544 $resp->{copy_holds} = $e->search_action_hold_request(
1546 hold_type => OILS_HOLD_TYPE_COPY,
1558 __PACKAGE__->register_method(
1559 method => 'uber_hold',
1561 api_name => 'open-ils.circ.hold.details.retrieve'
1565 my($self, $client, $auth, $hold_id) = @_;
1566 my $e = new_editor(authtoken=>$auth);
1567 $e->checkauth or return $e->event;
1568 $e->allowed('VIEW_HOLD') or return $e->event;
1572 my $hold = $e->retrieve_action_hold_request(
1577 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1580 ) or return $e->event;
1582 my $user = $hold->usr;
1583 $hold->usr($user->id);
1585 my $card = $e->retrieve_actor_card($user->card)
1586 or return $e->event;
1588 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1590 flesh_hold_notices([$hold], $e);
1591 flesh_hold_transits([$hold]);
1598 status => _hold_status($e, $hold),
1599 patron_first => $user->first_given_name,
1600 patron_last => $user->family_name,
1601 patron_barcode => $card->barcode,
1607 # -----------------------------------------------------
1608 # Returns the MVR object that represents what the
1610 # -----------------------------------------------------
1612 my( $e, $hold ) = @_;
1618 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1619 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1620 or return $e->event;
1621 $tid = $mr->master_record;
1623 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1624 $tid = $hold->target;
1626 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1627 $volume = $e->retrieve_asset_call_number($hold->target)
1628 or return $e->event;
1629 $tid = $volume->record;
1631 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1632 $copy = $e->retrieve_asset_copy($hold->target)
1633 or return $e->event;
1634 $volume = $e->retrieve_asset_call_number($copy->call_number)
1635 or return $e->event;
1636 $tid = $volume->record;
1639 if(!$copy and ref $hold->current_copy ) {
1640 $copy = $hold->current_copy;
1641 $hold->current_copy($copy->id);
1644 if(!$volume and $copy) {
1645 $volume = $e->retrieve_asset_call_number($copy->call_number);
1648 my $title = $e->retrieve_biblio_record_entry($tid);
1649 return ( $U->record_to_mvr($title), $volume, $copy );