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($recipient->home_ou) 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 $self->__user_hold_count($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 $self->delete_hold_copy_maps($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 = $self->update_hold_if_frozen($e, $hold);
531 $e->update_action_hold_request($hold)
532 or return $e->die_event;
538 # if the hold is frozen, this method ensures that the hold is not "targeted",
539 # that is, it clears the current_copy and prev_check_time to essentiallly
541 sub update_hold_if_frozen {
542 my($self, $e, $hold) = @_;
543 return undef if $hold->capture_time;
544 if($hold->frozen and ($hold->current_copy or $hold->prev_check_time)) {
545 $logger->info("clearing current_copy and check_time for frozen hold");
546 $hold->clear_current_copy;
547 $hold->clear_prev_check_time;
548 $e->update_action_hold_request($hold) or return $e->die_event;
554 __PACKAGE__->register_method(
555 method => "retrieve_hold_status",
556 api_name => "open-ils.circ.hold.status.retrieve",
558 Calculates the current status of the hold.
559 the requestor must have VIEW_HOLD permissions if the hold is for a user
560 other than the requestor.
561 Returns -1 on error (for now)
562 Returns 1 for 'waiting for copy to become available'
563 Returns 2 for 'waiting for copy capture'
564 Returns 3 for 'in transit'
565 Returns 4 for 'arrived'
568 sub retrieve_hold_status {
569 my($self, $client, $auth, $hold_id) = @_;
571 my $e = new_editor(authtoken => $auth);
572 return $e->event unless $e->checkauth;
573 my $hold = $e->retrieve_action_hold_request($hold_id)
576 if( $e->requestor->id != $hold->usr ) {
577 return $e->event unless $e->allowed('VIEW_HOLD');
580 return _hold_status($e, $hold);
586 return 1 unless $hold->current_copy;
587 return 2 unless $hold->capture_time;
589 my $copy = $hold->current_copy;
590 unless( ref $copy ) {
591 $copy = $e->retrieve_asset_copy($hold->current_copy)
595 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
596 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
602 #sub find_local_hold {
603 # my( $class, $session, $copy, $user ) = @_;
604 # return $class->find_nearest_permitted_hold($session, $copy, $user);
608 sub fetch_open_hold_by_current_copy {
611 my $hold = $apputils->simplereq(
613 'open-ils.cstore.direct.action.hold_request.search.atomic',
614 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
615 return $hold->[0] if ref($hold);
619 sub fetch_related_holds {
622 return $apputils->simplereq(
624 'open-ils.cstore.direct.action.hold_request.search.atomic',
625 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
629 __PACKAGE__->register_method (
630 method => "hold_pull_list",
631 api_name => "open-ils.circ.hold_pull_list.retrieve",
633 Returns a list of holds that need to be "pulled"
638 __PACKAGE__->register_method (
639 method => "hold_pull_list",
640 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
642 Returns a list of hold ID's that need to be "pulled"
649 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
650 my( $reqr, $evt ) = $U->checkses($authtoken);
653 my $org = $reqr->ws_ou || $reqr->home_ou;
654 # the perm locaiton shouldn't really matter here since holds
655 # will exist all over and VIEW_HOLDS should be universal
656 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
659 if( $self->api_name =~ /id_list/ ) {
660 return $U->storagereq(
661 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
662 $org, $limit, $offset );
664 return $U->storagereq(
665 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
666 $org, $limit, $offset );
670 __PACKAGE__->register_method (
671 method => 'fetch_hold_notify',
672 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
674 Returns a list of hold notification objects based on hold id.
675 @param authtoken The loggin session key
676 @param holdid The id of the hold whose notifications we want to retrieve
677 @return An array of hold notification objects, event on error.
681 sub fetch_hold_notify {
682 my( $self, $conn, $authtoken, $holdid ) = @_;
683 my( $requestor, $evt ) = $U->checkses($authtoken);
686 ($hold, $evt) = $U->fetch_hold($holdid);
688 ($patron, $evt) = $U->fetch_user($hold->usr);
691 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
694 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
695 return $U->cstorereq(
696 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
700 __PACKAGE__->register_method (
701 method => 'create_hold_notify',
702 api_name => 'open-ils.circ.hold_notification.create',
704 Creates a new hold notification object
705 @param authtoken The login session key
706 @param notification The hold notification object to create
707 @return ID of the new object on success, Event on error
711 sub __create_hold_notify {
712 my( $self, $conn, $authtoken, $notification ) = @_;
713 my( $requestor, $evt ) = $U->checkses($authtoken);
716 ($hold, $evt) = $U->fetch_hold($notification->hold);
718 ($patron, $evt) = $U->fetch_user($hold->usr);
721 # XXX perm depth probably doesn't matter here -- should always be consortium level
722 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
725 # Set the proper notifier
726 $notification->notify_staff($requestor->id);
727 my $id = $U->storagereq(
728 'open-ils.storage.direct.action.hold_notification.create', $notification );
729 return $U->DB_UPDATE_FAILED($notification) unless $id;
730 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
735 sub create_hold_notify {
736 my( $self, $conn, $auth, $note ) = @_;
737 my $e = new_editor(authtoken=>$auth, xact=>1);
738 return $e->die_event unless $e->checkauth;
740 my $hold = $e->retrieve_action_hold_request($note->hold)
741 or return $e->die_event;
742 my $patron = $e->retrieve_actor_user($hold->usr)
743 or return $e->die_event;
745 return $e->die_event unless
746 $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
748 $note->notify_staff($e->requestor->id);
749 $e->create_action_hold_notification($note) or return $e->die_event;
755 __PACKAGE__->register_method(
756 method => 'reset_hold',
757 api_name => 'open-ils.circ.hold.reset',
759 Un-captures and un-targets a hold, essentially returning
760 it to the state it was in directly after it was placed,
761 then attempts to re-target the hold
762 @param authtoken The login session key
763 @param holdid The id of the hold
769 my( $self, $conn, $auth, $holdid ) = @_;
771 my ($hold, $evt) = $U->fetch_hold($holdid);
773 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
775 $evt = $self->_reset_hold($reqr, $hold);
781 my ($self, $reqr, $hold) = @_;
783 my $e = new_editor(xact =>1, requestor => $reqr);
785 $logger->info("reseting hold ".$hold->id);
789 if( $hold->capture_time and $hold->current_copy ) {
791 my $copy = $e->retrieve_asset_copy($hold->current_copy)
794 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
795 $logger->info("setting copy to status 'reshelving' on hold retarget");
796 $copy->status(OILS_COPY_STATUS_RESHELVING);
797 $copy->editor($e->requestor->id);
798 $copy->edit_date('now');
799 $e->update_asset_copy($copy) or return $e->event;
801 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
803 # We don't want the copy to remain "in transit"
804 $copy->status(OILS_COPY_STATUS_RESHELVING);
805 $logger->warn("! reseting hold [$hid] that is in transit");
806 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
809 my $trans = $e->retrieve_action_transit_copy($transid);
811 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
812 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
813 $logger->info("Transit abort completed with result $evt");
814 return $evt unless "$evt" eq 1;
820 $hold->clear_capture_time;
821 $hold->clear_current_copy;
823 $e->update_action_hold_request($hold) or return $e->event;
827 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
833 __PACKAGE__->register_method(
834 method => 'fetch_open_title_holds',
835 api_name => 'open-ils.circ.open_holds.retrieve',
837 Returns a list ids of un-fulfilled holds for a given title id
838 @param authtoken The login session key
839 @param id the id of the item whose holds we want to retrieve
840 @param type The hold type - M, T, V, C
844 sub fetch_open_title_holds {
845 my( $self, $conn, $auth, $id, $type, $org ) = @_;
846 my $e = new_editor( authtoken => $auth );
847 return $e->event unless $e->checkauth;
850 $org ||= $e->requestor->ws_ou;
852 # return $e->search_action_hold_request(
853 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
855 # XXX make me return IDs in the future ^--
856 my $holds = $e->search_action_hold_request(
859 cancel_time => undef,
861 fulfillment_time => undef
865 flesh_hold_transits($holds);
870 sub flesh_hold_transits {
872 for my $hold ( @$holds ) {
874 $apputils->simplereq(
876 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
877 { hold => $hold->id },
878 { order_by => { ahtc => 'id desc' }, limit => 1 }
884 sub flesh_hold_notices {
885 my( $holds, $e ) = @_;
888 for my $hold (@$holds) {
889 my $notices = $e->search_action_hold_notification(
891 { hold => $hold->id },
892 { order_by => { anh => 'notify_time desc' } },
897 $hold->notify_count(scalar(@$notices));
899 my $n = $e->retrieve_action_hold_notification($$notices[0])
901 $hold->notify_time($n->notify_time);
909 __PACKAGE__->register_method(
910 method => 'fetch_captured_holds',
911 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
913 Returns a list of un-fulfilled holds for a given title id
914 @param authtoken The login session key
915 @param org The org id of the location in question
919 __PACKAGE__->register_method(
920 method => 'fetch_captured_holds',
921 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
923 Returns a list ids of un-fulfilled holds for a given title id
924 @param authtoken The login session key
925 @param org The org id of the location in question
929 sub fetch_captured_holds {
930 my( $self, $conn, $auth, $org ) = @_;
932 my $e = new_editor(authtoken => $auth);
933 return $e->event unless $e->checkauth;
934 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
936 $org ||= $e->requestor->ws_ou;
938 my $holds = $e->search_action_hold_request(
940 capture_time => { "!=" => undef },
941 current_copy => { "!=" => undef },
942 fulfillment_time => undef,
944 cancel_time => undef,
949 for my $h (@$holds) {
950 my $copy = $e->retrieve_asset_copy($h->current_copy)
953 $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
956 if( ! $self->api_name =~ /id_list/ ) {
957 flesh_hold_transits(\@res);
958 flesh_hold_notices(\@res, $e);
961 if( $self->api_name =~ /id_list/ ) {
962 return [ map { $_->id } @res ];
969 __PACKAGE__->register_method(
970 method => "check_title_hold",
971 api_name => "open-ils.circ.title_hold.is_possible",
973 Determines if a hold were to be placed by a given user,
974 whether or not said hold would have any potential copies
976 @param authtoken The login session key
977 @param params A hash of named params including:
978 patronid - the id of the hold recipient
979 titleid (brn) - the id of the title to be held
980 depth - the hold range depth (defaults to 0)
983 sub check_title_hold {
984 my( $self, $client, $authtoken, $params ) = @_;
986 my %params = %$params;
987 my $titleid = $params{titleid} ||"";
988 my $volid = $params{volume_id};
989 my $copyid = $params{copy_id};
990 my $mrid = $params{mrid} ||"";
991 my $depth = $params{depth} || 0;
992 my $pickup_lib = $params{pickup_lib};
993 my $hold_type = $params{hold_type} || 'T';
995 my $e = new_editor(authtoken=>$authtoken);
996 return $e->event unless $e->checkauth;
997 my $patron = $e->retrieve_actor_user($params{patronid})
1000 if( $e->requestor->id ne $patron->id ) {
1001 return $e->event unless
1002 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1005 return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
1007 my $rangelib = $params{range_lib} || $patron->home_ou;
1009 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1010 or return $e->event;
1012 $logger->info("checking hold possibility with type $hold_type");
1018 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1020 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1021 $volume = $e->retrieve_asset_call_number($copy->call_number)
1022 or return $e->event;
1023 $title = $e->retrieve_biblio_record_entry($volume->record)
1024 or return $e->event;
1025 return verify_copy_for_hold(
1026 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1028 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1030 $volume = $e->retrieve_asset_call_number($volid)
1031 or return $e->event;
1032 $title = $e->retrieve_biblio_record_entry($volume->record)
1033 or return $e->event;
1035 return _check_volume_hold_is_possible(
1036 $volume, $title, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1038 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1040 return _check_title_hold_is_possible(
1041 $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1043 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1045 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1046 my @recs = map { $_->source } @$maps;
1047 for my $rec (@recs) {
1048 return 1 if (_check_title_hold_is_possible(
1049 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
1057 sub ___check_title_hold_is_possible {
1058 my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1064 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1066 while( $title = $U->storagereq(
1067 'open-ils.storage.biblio.record_entry.ranged_tree',
1068 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1072 ref($title->call_numbers) and
1073 @{$title->call_numbers};
1075 for my $cn (@{$title->call_numbers}) {
1077 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1079 for my $copy (@{$cn->copies}) {
1080 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1081 return 1 if verify_copy_for_hold(
1082 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1083 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1094 sub _check_title_hold_is_possible {
1095 my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1097 my $e = new_editor();
1099 # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
1100 my $copies = $e->json_query(
1102 select => { acp => ['id', 'circ_lib'] },
1107 fkey => 'call_number',
1111 filter => { id => $titleid },
1116 acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
1117 ccs => { field => 'id', filter => { holdable => 't'}, fkey => 'status' }
1121 '+acp' => { circulate => 't', deleted => 'f', holdable => 't' }
1126 return $e->event unless defined $copies;
1127 $logger->info("title possible found ".scalar(@$copies)." potential copies");
1128 return 0 unless @$copies;
1130 # -----------------------------------------------------------------------
1131 # sort the copies into buckets based on their circ_lib proximity to
1132 # the patron's home_ou.
1133 # -----------------------------------------------------------------------
1135 my $home_org = $patron->home_ou;
1136 my $req_org = $request_lib->id;
1139 ($prox_cache{$home_org}) ?
1140 $prox_cache{$home_org} :
1141 $prox_cache{$home_org} = $e->search_actor_org_unit_proximity({from_org => $home_org});
1144 my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
1145 push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1147 my @keys = sort { $a <=> $b } keys %buckets;
1150 if( $home_org ne $req_org ) {
1151 # -----------------------------------------------------------------------
1152 # shove the copies close to the request_lib into the primary buckets
1153 # directly before the farthest away copies. That way, they are not
1154 # given priority, but they are checked before the farthest copies.
1155 # -----------------------------------------------------------------------
1157 ($prox_cache{$req_org}) ?
1158 $prox_cache{$req_org} :
1159 $prox_cache{$req_org} = $e->search_actor_org_unit_proximity({from_org => $req_org});
1162 my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
1163 push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
1165 my $highest_key = $keys[@keys - 1]; # the farthest prox in the exising buckets
1166 my $new_key = $highest_key - 0.5; # right before the farthest prox
1167 my @keys2 = sort { $a <=> $b } keys %buckets2;
1168 for my $key (@keys2) {
1169 last if $key >= $highest_key;
1170 push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
1174 @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 unless($title) { # grab the title if we don't already have it
1191 my $vol = $e->retrieve_asset_call_number(
1192 [ $copy->call_number, { flesh => 1, flesh_fields => { acn => ['record'] } } ] );
1193 $title = $vol->record;
1196 return 1 if verify_copy_for_hold(
1197 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1206 sub _check_volume_hold_is_possible {
1207 my( $vol, $title, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1208 my $copies = new_editor->search_asset_copy({call_number => $vol->id});
1209 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1210 for my $copy ( @$copies ) {
1211 return 1 if verify_copy_for_hold(
1212 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1219 sub verify_copy_for_hold {
1220 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1221 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1222 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1223 { patron => $patron,
1224 requestor => $requestor,
1227 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1228 pickup_lib => $pickup_lib,
1229 request_lib => $request_lib
1237 sub find_nearest_permitted_hold {
1240 my $editor = shift; # CStoreEditor object
1241 my $copy = shift; # copy to target
1242 my $user = shift; # staff
1243 my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1244 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1246 my $bc = $copy->barcode;
1248 # find any existing holds that already target this copy
1249 my $old_holds = $editor->search_action_hold_request(
1250 { current_copy => $copy->id,
1251 cancel_time => undef,
1252 capture_time => undef
1256 # hold->type "R" means we need this copy
1257 for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1260 my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.hold_stalling.soft');
1262 $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
1263 " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
1265 # search for what should be the best holds for this copy to fulfill
1266 my $best_holds = $U->storagereq(
1267 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1268 $user->ws_ou, $copy->id, 10, $hold_stall_interval );
1270 unless(@$best_holds) {
1272 if( my $hold = $$old_holds[0] ) {
1273 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1277 $logger->info("circulator: no suitable holds found for copy $bc");
1278 return (undef, $evt);
1284 # for each potential hold, we have to run the permit script
1285 # to make sure the hold is actually permitted.
1286 for my $holdid (@$best_holds) {
1287 next unless $holdid;
1288 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1290 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1291 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1292 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1294 # see if this hold is permitted
1295 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1296 { patron_id => $hold->usr,
1299 pickup_lib => $hold->pickup_lib,
1300 request_lib => $rlib,
1311 unless( $best_hold ) { # no "good" permitted holds were found
1312 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1313 $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1318 $logger->info("circulator: no suitable holds found for copy $bc");
1319 return (undef, $evt);
1322 $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1324 # indicate a permitted hold was found
1325 return $best_hold if $check_only;
1327 # we've found a permitted hold. we need to "grab" the copy
1328 # to prevent re-targeted holds (next part) from re-grabbing the copy
1329 $best_hold->current_copy($copy->id);
1330 $editor->update_action_hold_request($best_hold)
1331 or return (undef, $editor->event);
1336 # re-target any other holds that already target this copy
1337 for my $old_hold (@$old_holds) {
1338 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1339 $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
1340 $old_hold->id." after a better hold [".$best_hold->id."] was found");
1341 $old_hold->clear_current_copy;
1342 $old_hold->clear_prev_check_time;
1343 $editor->update_action_hold_request($old_hold)
1344 or return (undef, $editor->event);
1348 return ($best_hold, undef, $retarget);
1356 __PACKAGE__->register_method(
1357 method => 'all_rec_holds',
1358 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1362 my( $self, $conn, $auth, $title_id, $args ) = @_;
1364 my $e = new_editor(authtoken=>$auth);
1365 $e->checkauth or return $e->event;
1366 $e->allowed('VIEW_HOLD') or return $e->event;
1368 $args ||= { fulfillment_time => undef };
1369 $args->{cancel_time} = undef;
1371 my $resp = { volume_holds => [], copy_holds => [] };
1373 $resp->{title_holds} = $e->search_action_hold_request(
1375 hold_type => OILS_HOLD_TYPE_TITLE,
1376 target => $title_id,
1380 my $vols = $e->search_asset_call_number(
1381 { record => $title_id, deleted => 'f' }, {idlist=>1});
1383 return $resp unless @$vols;
1385 $resp->{volume_holds} = $e->search_action_hold_request(
1387 hold_type => OILS_HOLD_TYPE_VOLUME,
1392 my $copies = $e->search_asset_copy(
1393 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1395 return $resp unless @$copies;
1397 $resp->{copy_holds} = $e->search_action_hold_request(
1399 hold_type => OILS_HOLD_TYPE_COPY,
1411 __PACKAGE__->register_method(
1412 method => 'uber_hold',
1413 api_name => 'open-ils.circ.hold.details.retrieve'
1417 my($self, $client, $auth, $hold_id) = @_;
1418 my $e = new_editor(authtoken=>$auth);
1419 $e->checkauth or return $e->event;
1420 $e->allowed('VIEW_HOLD') or return $e->event;
1424 my $hold = $e->retrieve_action_hold_request(
1429 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1432 ) or return $e->event;
1434 my $user = $hold->usr;
1435 $hold->usr($user->id);
1437 my $card = $e->retrieve_actor_card($user->card)
1438 or return $e->event;
1440 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1442 flesh_hold_notices([$hold], $e);
1443 flesh_hold_transits([$hold]);
1450 status => _hold_status($e, $hold),
1451 patron_first => $user->first_given_name,
1452 patron_last => $user->family_name,
1453 patron_barcode => $card->barcode,
1459 # -----------------------------------------------------
1460 # Returns the MVR object that represents what the
1462 # -----------------------------------------------------
1464 my( $e, $hold ) = @_;
1470 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1471 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1472 or return $e->event;
1473 $tid = $mr->master_record;
1475 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1476 $tid = $hold->target;
1478 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1479 $volume = $e->retrieve_asset_call_number($hold->target)
1480 or return $e->event;
1481 $tid = $volume->record;
1483 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1484 $copy = $e->retrieve_asset_copy($hold->target)
1485 or return $e->event;
1486 $volume = $e->retrieve_asset_call_number($copy->call_number)
1487 or return $e->event;
1488 $tid = $volume->record;
1491 if(!$copy and ref $hold->current_copy ) {
1492 $copy = $hold->current_copy;
1493 $hold->current_copy($copy->id);
1496 if(!$volume and $copy) {
1497 $volume = $e->retrieve_asset_call_number($copy->call_number);
1500 my $title = $e->retrieve_biblio_record_entry($tid);
1501 return ( $U->record_to_mvr($title), $volume, $copy );