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/OpenSRF::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->checkperm($rid, $porg, 'MR_HOLDS'); }
118 if( $t eq OILS_HOLD_TYPE_TITLE )
119 { $pevt = $e->event unless $e->checkperm($rid, $porg, 'TITLE_HOLDS'); }
121 if( $t eq OILS_HOLD_TYPE_VOLUME )
122 { $pevt = $e->event unless $e->checkperm($rid, $porg, 'VOLUME_HOLDS'); }
124 if( $t eq OILS_HOLD_TYPE_COPY )
125 { $pevt = $e->event unless $e->checkperm($rid, $porg, 'COPY_HOLDS'); }
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",
304 api_name => "open-ils.circ.holds.id_list.retrieve",
306 Retrieves all the hold ids for the specified
307 user id. The login session is the requestor and if the requestor is
308 different from the user, then the requestor must have VIEW_HOLD permissions.
312 my($self, $client, $login_session, $user_id) = @_;
314 my( $user, $target, $evt ) = $apputils->checkses_requestor(
315 $login_session, $user_id, 'VIEW_HOLD' );
318 my $holds = $apputils->simplereq(
320 "open-ils.cstore.direct.action.hold_request.search.atomic",
323 fulfillment_time => undef,
324 cancel_time => undef,
326 { order_by => { ahr => "request_time" } }
329 if( ! $self->api_name =~ /id_list/ ) {
330 for my $hold ( @$holds ) {
332 $apputils->simplereq(
334 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
335 { hold => $hold->id },
336 { order_by => { ahtc => 'id desc' }, limit => 1 }
342 if( $self->api_name =~ /id_list/ ) {
343 return [ map { $_->id } @$holds ];
350 __PACKAGE__->register_method(
351 method => 'user_hold_count',
352 api_name => 'open-ils.circ.hold.user.count');
354 sub user_hold_count {
355 my( $self, $conn, $auth, $userid ) = @_;
356 my $e = new_editor(authtoken=>$auth);
357 return $e->event unless $e->checkauth;
358 my $patron = $e->retrieve_actor_user($userid)
360 return $e->event unless $e->allowed('VIEW_HOLD', $patron->home_ou);
361 return __user_hold_count($self, $e, $userid);
364 sub __user_hold_count {
365 my( $self, $e, $userid ) = @_;
366 my $holds = $e->search_action_hold_request(
368 fulfillment_time => undef,
369 cancel_time => undef,
374 return scalar(@$holds);
378 __PACKAGE__->register_method(
379 method => "retrieve_holds_by_pickup_lib",
380 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
382 Retrieves all the holds, with hold transits attached, for the specified
386 __PACKAGE__->register_method(
387 method => "retrieve_holds_by_pickup_lib",
388 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
390 Retrieves all the hold ids for the specified
394 sub retrieve_holds_by_pickup_lib {
395 my($self, $client, $login_session, $ou_id) = @_;
397 #FIXME -- put an appropriate permission check here
398 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
399 # $login_session, $user_id, 'VIEW_HOLD' );
400 #return $evt if $evt;
402 my $holds = $apputils->simplereq(
404 "open-ils.cstore.direct.action.hold_request.search.atomic",
406 pickup_lib => $ou_id ,
407 fulfillment_time => undef,
410 { order_by => { ahr => "request_time" } });
413 if( ! $self->api_name =~ /id_list/ ) {
414 flesh_hold_transits($holds);
417 if( $self->api_name =~ /id_list/ ) {
418 return [ map { $_->id } @$holds ];
424 __PACKAGE__->register_method(
425 method => "cancel_hold",
426 api_name => "open-ils.circ.hold.cancel",
428 Cancels the specified hold. The login session
429 is the requestor and if the requestor is different from the usr field
430 on the hold, the requestor must have CANCEL_HOLDS permissions.
431 the hold may be either the hold object or the hold id
435 my($self, $client, $auth, $holdid) = @_;
437 my $e = new_editor(authtoken=>$auth, xact=>1);
438 return $e->event unless $e->checkauth;
440 my $hold = $e->retrieve_action_hold_request($holdid)
443 if( $e->requestor->id ne $hold->usr ) {
444 return $e->event unless $e->allowed('CANCEL_HOLDS');
447 return 1 if $hold->cancel_time;
449 # If the hold is captured, reset the copy status
450 if( $hold->capture_time and $hold->current_copy ) {
452 my $copy = $e->retrieve_asset_copy($hold->current_copy)
455 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
456 $logger->info("canceling hold $holdid whose item is on the holds shelf");
457 # $logger->info("setting copy to status 'reshelving' on hold cancel");
458 # $copy->status(OILS_COPY_STATUS_RESHELVING);
459 # $copy->editor($e->requestor->id);
460 # $copy->edit_date('now');
461 # $e->update_asset_copy($copy) or return $e->event;
463 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
466 $logger->warn("! canceling hold [$hid] that is in transit");
467 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
470 my $trans = $e->retrieve_action_transit_copy($transid);
471 # Leave the transit alive, but set the copy status to
472 # reshelving so it will be properly reshelved when it gets back home
474 $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
475 $e->update_action_transit_copy($trans) or return $e->die_event;
481 $hold->cancel_time('now');
482 $e->update_action_hold_request($hold)
485 delete_hold_copy_maps($self, $e, $hold->id);
491 sub delete_hold_copy_maps {
496 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
498 $editor->delete_action_hold_copy_map($_)
499 or return $editor->event;
505 __PACKAGE__->register_method(
506 method => "update_hold",
507 api_name => "open-ils.circ.hold.update",
509 Updates the specified hold. The login session
510 is the requestor and if the requestor is different from the usr field
511 on the hold, the requestor must have UPDATE_HOLDS permissions.
515 my($self, $client, $auth, $hold) = @_;
517 my $e = new_editor(authtoken=>$auth, xact=>1);
518 return $e->die_event unless $e->checkauth;
520 if($hold->usr ne $e->requestor->id) {
521 # if the hold is for a different user, make sure the
522 # requestor has the appropriate permissions
523 my $usr = $e->retrieve_actor_user($hold->usr)
524 or return $e->die_event;
525 return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
528 my $evt = update_hold_if_frozen($self, $e, $hold);
531 $e->update_action_hold_request($hold)
532 or return $e->die_event;
539 # if the hold is frozen, this method ensures that the hold is not "targeted",
540 # that is, it clears the current_copy and prev_check_time to essentiallly
542 sub update_hold_if_frozen {
543 my($self, $e, $hold) = @_;
544 return undef if $hold->capture_time;
545 if($hold->frozen and ($hold->current_copy or $hold->prev_check_time)) {
546 $logger->info("clearing current_copy and check_time for frozen hold");
547 $hold->clear_current_copy;
548 $hold->clear_prev_check_time;
549 $e->update_action_hold_request($hold) or return $e->die_event;
555 __PACKAGE__->register_method(
556 method => "retrieve_hold_status",
557 api_name => "open-ils.circ.hold.status.retrieve",
559 Calculates the current status of the hold.
560 the requestor must have VIEW_HOLD permissions if the hold is for a user
561 other than the requestor.
562 Returns -1 on error (for now)
563 Returns 1 for 'waiting for copy to become available'
564 Returns 2 for 'waiting for copy capture'
565 Returns 3 for 'in transit'
566 Returns 4 for 'arrived'
569 sub retrieve_hold_status {
570 my($self, $client, $auth, $hold_id) = @_;
572 my $e = new_editor(authtoken => $auth);
573 return $e->event unless $e->checkauth;
574 my $hold = $e->retrieve_action_hold_request($hold_id)
577 if( $e->requestor->id != $hold->usr ) {
578 return $e->event unless $e->allowed('VIEW_HOLD');
581 return _hold_status($e, $hold);
587 return 1 unless $hold->current_copy;
588 return 2 unless $hold->capture_time;
590 my $copy = $hold->current_copy;
591 unless( ref $copy ) {
592 $copy = $e->retrieve_asset_copy($hold->current_copy)
596 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
597 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
603 #sub find_local_hold {
604 # my( $class, $session, $copy, $user ) = @_;
605 # return $class->find_nearest_permitted_hold($session, $copy, $user);
609 sub fetch_open_hold_by_current_copy {
612 my $hold = $apputils->simplereq(
614 'open-ils.cstore.direct.action.hold_request.search.atomic',
615 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
616 return $hold->[0] if ref($hold);
620 sub fetch_related_holds {
623 return $apputils->simplereq(
625 'open-ils.cstore.direct.action.hold_request.search.atomic',
626 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
630 __PACKAGE__->register_method (
631 method => "hold_pull_list",
632 api_name => "open-ils.circ.hold_pull_list.retrieve",
634 Returns a list of holds that need to be "pulled"
639 __PACKAGE__->register_method (
640 method => "hold_pull_list",
641 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
643 Returns a list of hold ID's that need to be "pulled"
650 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
651 my( $reqr, $evt ) = $U->checkses($authtoken);
654 my $org = $reqr->ws_ou || $reqr->home_ou;
655 # the perm locaiton shouldn't really matter here since holds
656 # will exist all over and VIEW_HOLDS should be universal
657 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
660 if( $self->api_name =~ /id_list/ ) {
661 return $U->storagereq(
662 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
663 $org, $limit, $offset );
665 return $U->storagereq(
666 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
667 $org, $limit, $offset );
671 __PACKAGE__->register_method (
672 method => 'fetch_hold_notify',
673 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
675 Returns a list of hold notification objects based on hold id.
676 @param authtoken The loggin session key
677 @param holdid The id of the hold whose notifications we want to retrieve
678 @return An array of hold notification objects, event on error.
682 sub fetch_hold_notify {
683 my( $self, $conn, $authtoken, $holdid ) = @_;
684 my( $requestor, $evt ) = $U->checkses($authtoken);
687 ($hold, $evt) = $U->fetch_hold($holdid);
689 ($patron, $evt) = $U->fetch_user($hold->usr);
692 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
695 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
696 return $U->cstorereq(
697 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
701 __PACKAGE__->register_method (
702 method => 'create_hold_notify',
703 api_name => 'open-ils.circ.hold_notification.create',
705 Creates a new hold notification object
706 @param authtoken The login session key
707 @param notification The hold notification object to create
708 @return ID of the new object on success, Event on error
712 sub __create_hold_notify {
713 my( $self, $conn, $authtoken, $notification ) = @_;
714 my( $requestor, $evt ) = $U->checkses($authtoken);
717 ($hold, $evt) = $U->fetch_hold($notification->hold);
719 ($patron, $evt) = $U->fetch_user($hold->usr);
722 # XXX perm depth probably doesn't matter here -- should always be consortium level
723 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
726 # Set the proper notifier
727 $notification->notify_staff($requestor->id);
728 my $id = $U->storagereq(
729 'open-ils.storage.direct.action.hold_notification.create', $notification );
730 return $U->DB_UPDATE_FAILED($notification) unless $id;
731 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
736 sub create_hold_notify {
737 my( $self, $conn, $auth, $note ) = @_;
738 my $e = new_editor(authtoken=>$auth, xact=>1);
739 return $e->die_event unless $e->checkauth;
741 my $hold = $e->retrieve_action_hold_request($note->hold)
742 or return $e->die_event;
743 my $patron = $e->retrieve_actor_user($hold->usr)
744 or return $e->die_event;
746 return $e->die_event unless
747 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
749 $note->notify_staff($e->requestor->id);
750 $e->create_action_hold_notification($note) or return $e->die_event;
756 __PACKAGE__->register_method(
757 method => 'reset_hold',
758 api_name => 'open-ils.circ.hold.reset',
760 Un-captures and un-targets a hold, essentially returning
761 it to the state it was in directly after it was placed,
762 then attempts to re-target the hold
763 @param authtoken The login session key
764 @param holdid The id of the hold
770 my( $self, $conn, $auth, $holdid ) = @_;
772 my ($hold, $evt) = $U->fetch_hold($holdid);
774 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
776 $evt = _reset_hold($self, $reqr, $hold);
782 my ($self, $reqr, $hold) = @_;
784 my $e = new_editor(xact =>1, requestor => $reqr);
786 $logger->info("reseting hold ".$hold->id);
790 if( $hold->capture_time and $hold->current_copy ) {
792 my $copy = $e->retrieve_asset_copy($hold->current_copy)
795 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
796 $logger->info("setting copy to status 'reshelving' on hold retarget");
797 $copy->status(OILS_COPY_STATUS_RESHELVING);
798 $copy->editor($e->requestor->id);
799 $copy->edit_date('now');
800 $e->update_asset_copy($copy) or return $e->event;
802 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
804 # We don't want the copy to remain "in transit"
805 $copy->status(OILS_COPY_STATUS_RESHELVING);
806 $logger->warn("! reseting hold [$hid] that is in transit");
807 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
810 my $trans = $e->retrieve_action_transit_copy($transid);
812 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
813 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
814 $logger->info("Transit abort completed with result $evt");
815 return $evt unless "$evt" eq 1;
821 $hold->clear_capture_time;
822 $hold->clear_current_copy;
824 $e->update_action_hold_request($hold) or return $e->event;
828 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
834 __PACKAGE__->register_method(
835 method => 'fetch_open_title_holds',
836 api_name => 'open-ils.circ.open_holds.retrieve',
838 Returns a list ids of un-fulfilled holds for a given title id
839 @param authtoken The login session key
840 @param id the id of the item whose holds we want to retrieve
841 @param type The hold type - M, T, V, C
845 sub fetch_open_title_holds {
846 my( $self, $conn, $auth, $id, $type, $org ) = @_;
847 my $e = new_editor( authtoken => $auth );
848 return $e->event unless $e->checkauth;
851 $org ||= $e->requestor->ws_ou;
853 # return $e->search_action_hold_request(
854 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
856 # XXX make me return IDs in the future ^--
857 my $holds = $e->search_action_hold_request(
860 cancel_time => undef,
862 fulfillment_time => undef
866 flesh_hold_transits($holds);
871 sub flesh_hold_transits {
873 for my $hold ( @$holds ) {
875 $apputils->simplereq(
877 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
878 { hold => $hold->id },
879 { order_by => { ahtc => 'id desc' }, limit => 1 }
885 sub flesh_hold_notices {
886 my( $holds, $e ) = @_;
889 for my $hold (@$holds) {
890 my $notices = $e->search_action_hold_notification(
892 { hold => $hold->id },
893 { order_by => { anh => 'notify_time desc' } },
898 $hold->notify_count(scalar(@$notices));
900 my $n = $e->retrieve_action_hold_notification($$notices[0])
902 $hold->notify_time($n->notify_time);
910 __PACKAGE__->register_method(
911 method => 'fetch_captured_holds',
912 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
914 Returns a list of un-fulfilled holds for a given title id
915 @param authtoken The login session key
916 @param org The org id of the location in question
920 __PACKAGE__->register_method(
921 method => 'fetch_captured_holds',
922 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
924 Returns a list ids of un-fulfilled holds for a given title id
925 @param authtoken The login session key
926 @param org The org id of the location in question
930 sub fetch_captured_holds {
931 my( $self, $conn, $auth, $org ) = @_;
933 my $e = new_editor(authtoken => $auth);
934 return $e->event unless $e->checkauth;
935 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
937 $org ||= $e->requestor->ws_ou;
939 my $holds = $e->search_action_hold_request(
941 capture_time => { "!=" => undef },
942 current_copy => { "!=" => undef },
943 fulfillment_time => undef,
945 cancel_time => undef,
950 for my $h (@$holds) {
951 my $copy = $e->retrieve_asset_copy($h->current_copy)
954 $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
957 if( ! $self->api_name =~ /id_list/ ) {
958 flesh_hold_transits(\@res);
959 flesh_hold_notices(\@res, $e);
962 if( $self->api_name =~ /id_list/ ) {
963 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';
996 my $e = new_editor(authtoken=>$authtoken);
997 return $e->event unless $e->checkauth;
998 my $patron = $e->retrieve_actor_user($params{patronid})
1001 if( $e->requestor->id ne $patron->id ) {
1002 return $e->event unless
1003 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1006 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1008 my $rangelib = $params{range_lib} || $patron->home_ou;
1010 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1011 or return $e->event;
1013 $logger->info("checking hold possibility with type $hold_type");
1019 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1021 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1022 $volume = $e->retrieve_asset_call_number($copy->call_number)
1023 or return $e->event;
1024 $title = $e->retrieve_biblio_record_entry($volume->record)
1025 or return $e->event;
1026 return verify_copy_for_hold(
1027 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1029 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1031 $volume = $e->retrieve_asset_call_number($volid)
1032 or return $e->event;
1033 $title = $e->retrieve_biblio_record_entry($volume->record)
1034 or return $e->event;
1036 return _check_volume_hold_is_possible(
1037 $volume, $title, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1039 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1041 return _check_title_hold_is_possible(
1042 $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1044 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1046 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1047 my @recs = map { $_->source } @$maps;
1048 for my $rec (@recs) {
1049 return 1 if (_check_title_hold_is_possible(
1050 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
1058 sub ___check_title_hold_is_possible {
1059 my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1065 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1067 while( $title = $U->storagereq(
1068 'open-ils.storage.biblio.record_entry.ranged_tree',
1069 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1073 ref($title->call_numbers) and
1074 @{$title->call_numbers};
1076 for my $cn (@{$title->call_numbers}) {
1078 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1080 for my $copy (@{$cn->copies}) {
1081 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1082 return 1 if verify_copy_for_hold(
1083 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1084 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1095 sub _check_metarecord_hold_is_possible {
1096 my( $mrid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1098 my $e = new_editor();
1100 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given metarecord
1101 my $copies = $e->json_query(
1103 select => { acp => ['id', 'circ_lib'] },
1108 fkey => 'call_number',
1113 filter => { metarecord => $mrid }
1117 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1118 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1122 '+acp' => { circulate => 't', deleted => 'f', holdable => 't' }
1127 return $e->event unless defined $copies;
1128 $logger->info("metarecord possible found ".scalar(@$copies)." potential copies");
1129 return 0 unless @$copies;
1131 # -----------------------------------------------------------------------
1132 # sort the copies into buckets based on their circ_lib proximity to
1133 # the patron's home_ou.
1134 # -----------------------------------------------------------------------
1136 my $home_org = $patron->home_ou;
1137 my $req_org = $request_lib->id;
1140 ($prox_cache{$home_org}) ?
1141 $prox_cache{$home_org} :
1142 $prox_cache{$home_org} = $e->search_actor_org_unit_proximity({from_org => $home_org});
1145 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1146 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1148 my @keys = sort { $a <=> $b } keys %buckets;
1151 if( $home_org ne $req_org ) {
1152 # -----------------------------------------------------------------------
1153 # shove the copies close to the request_lib into the primary buckets
1154 # directly before the farthest away copies. That way, they are not
1155 # given priority, but they are checked before the farthest copies.
1156 # -----------------------------------------------------------------------
1158 ($prox_cache{$req_org}) ?
1159 $prox_cache{$req_org} :
1160 $prox_cache{$req_org} = $e->search_actor_org_unit_proximity({from_org => $req_org});
1163 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1164 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1166 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1167 my $new_key = $highest_key - 0.5; # right before the farthest prox
1168 my @keys2 = sort { $a <=> $b } keys %buckets2;
1169 for my $key (@keys2) {
1170 last if $key >= $highest_key;
1171 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1175 @keys = sort { $a <=> $b } keys %buckets;
1178 for my $key (@keys) {
1179 my @cps = @{$buckets{$key}};
1181 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1183 for my $copyid (@cps) {
1185 next if $seen{$copyid};
1186 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1187 my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1188 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1190 my $vol = $e->retrieve_asset_call_number(
1191 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1193 return 1 if verify_copy_for_hold(
1194 $patron, $requestor, $vol->record, $copy, $pickup_lib, $request_lib );
1202 sub _check_title_hold_is_possible {
1203 my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1205 my $e = new_editor();
1207 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1208 my $copies = $e->json_query(
1210 select => { acp => ['id', 'circ_lib'] },
1215 fkey => 'call_number',
1219 filter => { id => $titleid },
1224 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1225 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1229 '+acp' => { circulate => 't', deleted => 'f', holdable => 't' }
1234 return $e->event unless defined $copies;
1235 $logger->info("title possible found ".scalar(@$copies)." potential copies");
1236 return 0 unless @$copies;
1238 # -----------------------------------------------------------------------
1239 # sort the copies into buckets based on their circ_lib proximity to
1240 # the patron's home_ou.
1241 # -----------------------------------------------------------------------
1243 my $home_org = $patron->home_ou;
1244 my $req_org = $request_lib->id;
1247 ($prox_cache{$home_org}) ?
1248 $prox_cache{$home_org} :
1249 $prox_cache{$home_org} = $e->search_actor_org_unit_proximity({from_org => $home_org});
1252 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1253 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1255 my @keys = sort { $a <=> $b } keys %buckets;
1258 if( $home_org ne $req_org ) {
1259 # -----------------------------------------------------------------------
1260 # shove the copies close to the request_lib into the primary buckets
1261 # directly before the farthest away copies. That way, they are not
1262 # given priority, but they are checked before the farthest copies.
1263 # -----------------------------------------------------------------------
1265 ($prox_cache{$req_org}) ?
1266 $prox_cache{$req_org} :
1267 $prox_cache{$req_org} = $e->search_actor_org_unit_proximity({from_org => $req_org});
1270 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1271 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1273 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1274 my $new_key = $highest_key - 0.5; # right before the farthest prox
1275 my @keys2 = sort { $a <=> $b } keys %buckets2;
1276 for my $key (@keys2) {
1277 last if $key >= $highest_key;
1278 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1282 @keys = sort { $a <=> $b } keys %buckets;
1286 for my $key (@keys) {
1287 my @cps = @{$buckets{$key}};
1289 $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
1291 for my $copyid (@cps) {
1293 next if $seen{$copyid};
1294 $seen{$copyid} = 1; # there could be dupes given the merged buckets
1295 my $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1296 $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
1298 unless($title) { # grab the title if we don't already have it
1299 my $vol = $e->retrieve_asset_call_number(
1300 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1301 $title = $vol->record;
1304 return 1 if verify_copy_for_hold(
1305 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1314 sub _check_volume_hold_is_possible {
1315 my( $vol, $title, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1316 my $copies = new_editor->search_asset_copy({call_number => $vol->id});
1317 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1318 for my $copy ( @$copies ) {
1319 return 1 if verify_copy_for_hold(
1320 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1327 sub verify_copy_for_hold {
1328 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1329 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1330 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1331 { patron => $patron,
1332 requestor => $requestor,
1335 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1336 pickup_lib => $pickup_lib,
1337 request_lib => $request_lib
1345 sub find_nearest_permitted_hold {
1348 my $editor = shift; # CStoreEditor object
1349 my $copy = shift; # copy to target
1350 my $user = shift; # staff
1351 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1352 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1354 my $bc = $copy->barcode;
1356 # find any existing holds that already target this copy
1357 my $old_holds = $editor->search_action_hold_request(
1358 { current_copy => $copy->id,
1359 cancel_time => undef,
1360 capture_time => undef
1364 # hold->type "R" means we need this copy
1365 for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1368 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.hold_stalling.soft');
1370 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1371 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1373 # search for what should be the best holds for this copy to fulfill
1374 my $best_holds = $U->storagereq(
1375 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1376 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1378 unless(@$best_holds) {
1380 if( my $hold = $$old_holds[0] ) {
1381 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1385 $logger->info("circulator: no suitable holds found for copy $bc");
1386 return (undef, $evt);
1392 # for each potential hold, we have to run the permit script
1393 # to make sure the hold is actually permitted.
1394 for my $holdid (@$best_holds) {
1395 next unless $holdid;
1396 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1398 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1399 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1400 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1402 # see if this hold is permitted
1403 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1404 { patron_id => $hold->usr,
1407 pickup_lib => $hold->pickup_lib,
1408 request_lib => $rlib,
1419 unless( $best_hold ) { # no "good" permitted holds were found
1420 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1421 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1426 $logger->info("circulator: no suitable holds found for copy $bc");
1427 return (undef, $evt);
1430 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1432 # indicate a permitted hold was found
1433 return $best_hold if $check_only;
1435 # we've found a permitted hold. we need to "grab" the copy
1436 # to prevent re-targeted holds (next part) from re-grabbing the copy
1437 $best_hold->current_copy($copy->id);
1438 $editor->update_action_hold_request($best_hold)
1439 or return (undef, $editor->event);
1444 # re-target any other holds that already target this copy
1445 for my $old_hold (@$old_holds) {
1446 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1447 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1448 $old_hold->id." after a better hold [".$best_hold->id."] was found");
1449 $old_hold->clear_current_copy;
1450 $old_hold->clear_prev_check_time;
1451 $editor->update_action_hold_request($old_hold)
1452 or return (undef, $editor->event);
1456 return ($best_hold, undef, $retarget);
1464 __PACKAGE__->register_method(
1465 method => 'all_rec_holds',
1466 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1470 my( $self, $conn, $auth, $title_id, $args ) = @_;
1472 my $e = new_editor(authtoken=>$auth);
1473 $e->checkauth or return $e->event;
1474 $e->allowed('VIEW_HOLD') or return $e->event;
1476 $args ||= { fulfillment_time => undef };
1477 $args->{cancel_time} = undef;
1479 my $resp = { volume_holds => [], copy_holds => [] };
1481 $resp->{title_holds} = $e->search_action_hold_request(
1483 hold_type => OILS_HOLD_TYPE_TITLE,
1484 target => $title_id,
1488 my $vols = $e->search_asset_call_number(
1489 { record => $title_id, deleted => 'f' }, {idlist=>1});
1491 return $resp unless @$vols;
1493 $resp->{volume_holds} = $e->search_action_hold_request(
1495 hold_type => OILS_HOLD_TYPE_VOLUME,
1500 my $copies = $e->search_asset_copy(
1501 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1503 return $resp unless @$copies;
1505 $resp->{copy_holds} = $e->search_action_hold_request(
1507 hold_type => OILS_HOLD_TYPE_COPY,
1519 __PACKAGE__->register_method(
1520 method => 'uber_hold',
1521 api_name => 'open-ils.circ.hold.details.retrieve'
1525 my($self, $client, $auth, $hold_id) = @_;
1526 my $e = new_editor(authtoken=>$auth);
1527 $e->checkauth or return $e->event;
1528 $e->allowed('VIEW_HOLD') or return $e->event;
1532 my $hold = $e->retrieve_action_hold_request(
1537 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1540 ) or return $e->event;
1542 my $user = $hold->usr;
1543 $hold->usr($user->id);
1545 my $card = $e->retrieve_actor_card($user->card)
1546 or return $e->event;
1548 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1550 flesh_hold_notices([$hold], $e);
1551 flesh_hold_transits([$hold]);
1558 status => _hold_status($e, $hold),
1559 patron_first => $user->first_given_name,
1560 patron_last => $user->family_name,
1561 patron_barcode => $card->barcode,
1567 # -----------------------------------------------------
1568 # Returns the MVR object that represents what the
1570 # -----------------------------------------------------
1572 my( $e, $hold ) = @_;
1578 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1579 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1580 or return $e->event;
1581 $tid = $mr->master_record;
1583 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1584 $tid = $hold->target;
1586 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1587 $volume = $e->retrieve_asset_call_number($hold->target)
1588 or return $e->event;
1589 $tid = $volume->record;
1591 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1592 $copy = $e->retrieve_asset_copy($hold->target)
1593 or return $e->event;
1594 $volume = $e->retrieve_asset_call_number($copy->call_number)
1595 or return $e->event;
1596 $tid = $volume->record;
1599 if(!$copy and ref $hold->current_copy ) {
1600 $copy = $hold->current_copy;
1601 $hold->current_copy($copy->id);
1604 if(!$volume and $copy) {
1605 $volume = $e->retrieve_asset_call_number($copy->call_number);
1608 my $title = $e->retrieve_biblio_record_entry($tid);
1609 return ( $U->record_to_mvr($title), $volume, $copy );