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;
145 # push( @copyholds, $hold ) if $hold->hold_type eq OILS_HOLD_TYPE_COPY;
150 $conn->respond_complete(1);
152 # Go ahead and target the copy-level holds
154 'open-ils.storage.action.hold_request.copy_targeter',
155 undef, $_->id ) for @holds;
161 my( $self, $client, $login_session, @holds) = @_;
163 if(!@holds){return 0;}
164 my( $user, $evt ) = $apputils->checkses($login_session);
168 if(ref($holds[0]) eq 'ARRAY') {
170 } else { $holds = [ @holds ]; }
172 $logger->debug("Iterating over holds requests...");
174 for my $hold (@$holds) {
177 my $type = $hold->hold_type;
179 $logger->activity("User " . $user->id .
180 " creating new hold of type $type for user " . $hold->usr);
183 if($user->id ne $hold->usr) {
184 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
194 # am I allowed to place holds for this user?
195 if($hold->requestor ne $hold->usr) {
196 $perm = _check_request_holds_perm($user->id, $user->home_ou);
197 if($perm) { return $perm; }
200 # is this user allowed to have holds of this type?
201 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
203 #if there is a requestor, see if the requestor has override privelages
204 if($hold->requestor ne $hold->usr) {
205 $perm = _check_request_holds_override($user->id, $user->home_ou);
206 if($perm) {return $perm;}
213 #enforce the fact that the login is the one requesting the hold
214 $hold->requestor($user->id);
215 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
217 my $resp = $apputils->simplereq(
219 'open-ils.storage.direct.action.hold_request.create', $hold );
222 return OpenSRF::EX::ERROR ("Error creating hold");
229 # makes sure that a user has permission to place the type of requested hold
230 # returns the Perm exception if not allowed, returns undef if all is well
231 sub _check_holds_perm {
232 my($type, $user_id, $org_id) = @_;
236 if($evt = $apputils->check_perms(
237 $user_id, $org_id, "MR_HOLDS")) {
241 } elsif ($type eq "T") {
242 if($evt = $apputils->check_perms(
243 $user_id, $org_id, "TITLE_HOLDS")) {
247 } elsif($type eq "V") {
248 if($evt = $apputils->check_perms(
249 $user_id, $org_id, "VOLUME_HOLDS")) {
253 } elsif($type eq "C") {
254 if($evt = $apputils->check_perms(
255 $user_id, $org_id, "COPY_HOLDS")) {
263 # tests if the given user is allowed to place holds on another's behalf
264 sub _check_request_holds_perm {
267 if(my $evt = $apputils->check_perms(
268 $user_id, $org_id, "REQUEST_HOLDS")) {
273 sub _check_request_holds_override {
276 if(my $evt = $apputils->check_perms(
277 $user_id, $org_id, "REQUEST_HOLDS_OVERRIDE")) {
282 __PACKAGE__->register_method(
283 method => "retrieve_holds_by_id",
284 api_name => "open-ils.circ.holds.retrieve_by_id",
286 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
287 different from the user, then the requestor must have VIEW_HOLD permissions.
291 sub retrieve_holds_by_id {
292 my($self, $client, $auth, $hold_id) = @_;
293 my $e = new_editor(authtoken=>$auth);
294 $e->checkauth or return $e->event;
295 $e->allowed('VIEW_HOLD') or return $e->event;
297 my $holds = $e->search_action_hold_request(
299 { id => $hold_id , fulfillment_time => undef },
300 { order_by => { ahr => "request_time" } }
304 flesh_hold_transits($holds);
305 flesh_hold_notices($holds, $e);
310 __PACKAGE__->register_method(
311 method => "retrieve_holds",
312 api_name => "open-ils.circ.holds.retrieve",
314 Retrieves all the holds, with hold transits attached, for the specified
315 user id. The login session is the requestor and if the requestor is
316 different from the user, then the requestor must have VIEW_HOLD permissions.
319 __PACKAGE__->register_method(
320 method => "retrieve_holds",
321 api_name => "open-ils.circ.holds.id_list.retrieve",
323 Retrieves all the hold ids for the specified
324 user id. The login session is the requestor and if the requestor is
325 different from the user, then the requestor must have VIEW_HOLD permissions.
329 my($self, $client, $login_session, $user_id) = @_;
331 my( $user, $target, $evt ) = $apputils->checkses_requestor(
332 $login_session, $user_id, 'VIEW_HOLD' );
335 my $holds = $apputils->simplereq(
337 "open-ils.cstore.direct.action.hold_request.search.atomic",
340 fulfillment_time => undef,
341 cancel_time => undef,
343 { order_by => { ahr => "request_time" } }
346 if( ! $self->api_name =~ /id_list/ ) {
347 for my $hold ( @$holds ) {
349 $apputils->simplereq(
351 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
352 { hold => $hold->id },
353 { order_by => { ahtc => 'id desc' }, limit => 1 }
359 if( $self->api_name =~ /id_list/ ) {
360 return [ map { $_->id } @$holds ];
366 __PACKAGE__->register_method(
367 method => "retrieve_holds_by_pickup_lib",
368 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
370 Retrieves all the holds, with hold transits attached, for the specified
374 __PACKAGE__->register_method(
375 method => "retrieve_holds_by_pickup_lib",
376 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
378 Retrieves all the hold ids for the specified
382 sub retrieve_holds_by_pickup_lib {
383 my($self, $client, $login_session, $ou_id) = @_;
385 #FIXME -- put an appropriate permission check here
386 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
387 # $login_session, $user_id, 'VIEW_HOLD' );
388 #return $evt if $evt;
390 my $holds = $apputils->simplereq(
392 "open-ils.cstore.direct.action.hold_request.search.atomic",
394 pickup_lib => $ou_id ,
395 fulfillment_time => undef,
398 { order_by => { ahr => "request_time" } });
401 if( ! $self->api_name =~ /id_list/ ) {
402 flesh_hold_transits($holds);
405 if( $self->api_name =~ /id_list/ ) {
406 return [ map { $_->id } @$holds ];
412 __PACKAGE__->register_method(
413 method => "cancel_hold",
414 api_name => "open-ils.circ.hold.cancel",
416 Cancels the specified hold. The login session
417 is the requestor and if the requestor is different from the usr field
418 on the hold, the requestor must have CANCEL_HOLDS permissions.
419 the hold may be either the hold object or the hold id
423 my($self, $client, $auth, $holdid) = @_;
425 my $e = new_editor(authtoken=>$auth, xact=>1);
426 return $e->event unless $e->checkauth;
428 my $hold = $e->retrieve_action_hold_request($holdid)
431 if( $e->requestor->id ne $hold->usr ) {
432 return $e->event unless $e->allowed('CANCEL_HOLDS');
435 return 1 if $hold->cancel_time;
437 # If the hold is captured, reset the copy status
438 if( $hold->capture_time and $hold->current_copy ) {
440 my $copy = $e->retrieve_asset_copy($hold->current_copy)
443 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
444 $logger->info("setting copy to status 'reshelving' on hold cancel");
445 $copy->status(OILS_COPY_STATUS_RESHELVING);
446 $copy->editor($e->requestor->id);
447 $copy->edit_date('now');
448 $e->update_asset_copy($copy) or return $e->event;
450 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
453 $logger->warn("! canceling hold [$hid] that is in transit");
454 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
457 my $trans = $e->retrieve_action_transit_copy($transid);
459 $logger->info("Aborting transit [$transid] on hold [$hid] cancel...");
460 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
461 $logger->info("Transit abort completed with result $evt");
462 return $evt unless "$evt" eq 1;
466 # We don't want the copy to remain "in transit" or to recover
467 # any previous statuses
468 $logger->info("setting copy back to reshelving in hold+transit cancel");
469 $copy->status(OILS_COPY_STATUS_RESHELVING);
470 $copy->editor($e->requestor->id);
471 $copy->edit_date('now');
472 $e->update_asset_copy($copy) or return $e->event;
476 $hold->cancel_time('now');
477 $e->update_action_hold_request($hold)
480 $self->delete_hold_copy_maps($e, $hold->id);
486 sub delete_hold_copy_maps {
491 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
493 $editor->delete_action_hold_copy_map($_)
494 or return $editor->event;
500 __PACKAGE__->register_method(
501 method => "update_hold",
502 api_name => "open-ils.circ.hold.update",
504 Updates the specified hold. The login session
505 is the requestor and if the requestor is different from the usr field
506 on the hold, the requestor must have UPDATE_HOLDS permissions.
510 my($self, $client, $login_session, $hold) = @_;
512 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
513 $login_session, $hold->usr, 'UPDATE_HOLD' );
516 $logger->activity('User ' . $requestor->id .
517 ' updating hold ' . $hold->id . ' for user ' . $target->id );
519 return $U->storagereq(
520 "open-ils.storage.direct.action.hold_request.update", $hold );
524 __PACKAGE__->register_method(
525 method => "retrieve_hold_status",
526 api_name => "open-ils.circ.hold.status.retrieve",
528 Calculates the current status of the hold.
529 the requestor must have VIEW_HOLD permissions if the hold is for a user
530 other than the requestor.
531 Returns -1 on error (for now)
532 Returns 1 for 'waiting for copy to become available'
533 Returns 2 for 'waiting for copy capture'
534 Returns 3 for 'in transit'
535 Returns 4 for 'arrived'
538 sub retrieve_hold_status {
539 my($self, $client, $auth, $hold_id) = @_;
541 my $e = new_editor(authtoken => $auth);
542 return $e->event unless $e->checkauth;
543 my $hold = $e->retrieve_action_hold_request($hold_id)
546 if( $e->requestor->id != $hold->usr ) {
547 return $e->event unless $e->allowed('VIEW_HOLD');
550 return _hold_status($e, $hold);
556 return 1 unless $hold->current_copy;
557 return 2 unless $hold->capture_time;
559 my $copy = $hold->current_copy;
560 unless( ref $copy ) {
561 $copy = $e->retrieve_asset_copy($hold->current_copy)
565 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
566 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
576 __PACKAGE__->register_method(
577 method => "capture_copy",
578 api_name => "open-ils.circ.hold.capture_copy.barcode",
580 Captures a copy to fulfil a hold
581 Params is login session and copy barcode
582 Optional param is 'flesh'. If set, we also return the
583 relevant copy and title
584 login mus have COPY_CHECKIN permissions (since this is essentially
588 # XXX deprecate me XXX
591 my( $self, $client, $login_session, $params ) = @_;
592 my %params = %$params;
593 my $barcode = $params{barcode};
596 my( $user, $target, $copy, $hold, $evt );
598 ( $user, $evt ) = $apputils->checkses($login_session);
601 # am I allowed to checkin a copy?
602 $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
605 $logger->info("Capturing copy with barcode $barcode");
607 my $session = $apputils->start_db_session();
609 ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
612 $logger->debug("Capturing copy " . $copy->id);
614 #( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
615 ( $hold, $evt ) = $self->find_nearest_permitted_hold($session, $copy, $user);
618 warn "Found hold " . $hold->id . "\n";
619 $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
621 $hold->current_copy($copy->id);
622 $hold->capture_time("now");
625 my $stat = $session->request(
626 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
627 if(!$stat) { throw OpenSRF::EX::ERROR
628 ("Error updating hold request " . $copy->id); }
630 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF); #status on holds shelf
632 # if the staff member capturing this item is not at the pickup lib
633 if( $user->home_ou ne $hold->pickup_lib ) {
634 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
637 $copy->editor($user->id);
638 $copy->edit_date("now");
639 $stat = $session->request(
640 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
641 if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
643 my $payload = { hold => $hold };
644 $payload->{copy} = $copy if $params{flesh_copy};
646 if($params{flesh_record}) {
648 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
650 $record = $apputils->record_to_mvr($record);
651 $payload->{record} = $record;
654 $apputils->commit_db_session($session);
656 return OpenILS::Event->new('ROUTE_ITEM',
657 route_to => $hold->pickup_lib, payload => $payload );
660 sub _build_hold_transit {
661 my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
662 my $trans = Fieldmapper::action::hold_transit_copy->new;
664 $trans->hold($hold->id);
665 $trans->source($user->home_ou);
666 $trans->dest($hold->pickup_lib);
667 $trans->source_send_time("now");
668 $trans->target_copy($copy->id);
669 $trans->copy_status($copy->status);
671 my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
672 my ($stat) = $meth->run( $login_session, $trans, $session );
673 if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
674 else { $copy->status(6); } #status in transit
679 __PACKAGE__->register_method(
680 method => "create_hold_transit",
681 api_name => "open-ils.circ.hold_transit.create",
683 Creates a new transit object
686 sub create_hold_transit {
687 my( $self, $client, $login_session, $transit, $session ) = @_;
689 my( $user, $evt ) = $apputils->checkses($login_session);
691 $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
695 if($session) { $ses = $session; }
696 else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
698 return $ses->request(
699 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
705 sub find_local_hold {
706 my( $class, $session, $copy, $user ) = @_;
707 return $class->find_nearest_permitted_hold($session, $copy, $user);
715 sub fetch_open_hold_by_current_copy {
718 my $hold = $apputils->simplereq(
720 'open-ils.cstore.direct.action.hold_request.search.atomic',
721 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
722 return $hold->[0] if ref($hold);
726 sub fetch_related_holds {
729 return $apputils->simplereq(
731 'open-ils.cstore.direct.action.hold_request.search.atomic',
732 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
736 __PACKAGE__->register_method (
737 method => "hold_pull_list",
738 api_name => "open-ils.circ.hold_pull_list.retrieve",
740 Returns a list of holds that need to be "pulled"
745 __PACKAGE__->register_method (
746 method => "hold_pull_list",
747 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
749 Returns a list of hold ID's that need to be "pulled"
756 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
757 my( $reqr, $evt ) = $U->checkses($authtoken);
760 my $org = $reqr->ws_ou || $reqr->home_ou;
761 # the perm locaiton shouldn't really matter here since holds
762 # will exist all over and VIEW_HOLDS should be universal
763 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
766 if( $self->api_name =~ /id_list/ ) {
767 return $U->storagereq(
768 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
769 $org, $limit, $offset );
771 return $U->storagereq(
772 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
773 $org, $limit, $offset );
777 __PACKAGE__->register_method (
778 method => 'fetch_hold_notify',
779 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
781 Returns a list of hold notification objects based on hold id.
782 @param authtoken The loggin session key
783 @param holdid The id of the hold whose notifications we want to retrieve
784 @return An array of hold notification objects, event on error.
788 sub fetch_hold_notify {
789 my( $self, $conn, $authtoken, $holdid ) = @_;
790 my( $requestor, $evt ) = $U->checkses($authtoken);
793 ($hold, $evt) = $U->fetch_hold($holdid);
795 ($patron, $evt) = $U->fetch_user($hold->usr);
798 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
801 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
802 return $U->cstorereq(
803 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
807 __PACKAGE__->register_method (
808 method => 'create_hold_notify',
809 api_name => 'open-ils.circ.hold_notification.create',
811 Creates a new hold notification object
812 @param authtoken The login session key
813 @param notification The hold notification object to create
814 @return ID of the new object on success, Event on error
817 sub create_hold_notify {
818 my( $self, $conn, $authtoken, $notification ) = @_;
819 my( $requestor, $evt ) = $U->checkses($authtoken);
822 ($hold, $evt) = $U->fetch_hold($notification->hold);
824 ($patron, $evt) = $U->fetch_user($hold->usr);
827 # XXX perm depth probably doesn't matter here -- should always be consortium level
828 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
831 # Set the proper notifier
832 $notification->notify_staff($requestor->id);
833 my $id = $U->storagereq(
834 'open-ils.storage.direct.action.hold_notification.create', $notification );
835 return $U->DB_UPDATE_FAILED($notification) unless $id;
836 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
841 __PACKAGE__->register_method(
842 method => 'reset_hold',
843 api_name => 'open-ils.circ.hold.reset',
845 Un-captures and un-targets a hold, essentially returning
846 it to the state it was in directly after it was placed,
847 then attempts to re-target the hold
848 @param authtoken The login session key
849 @param holdid The id of the hold
855 my( $self, $conn, $auth, $holdid ) = @_;
857 my ($hold, $evt) = $U->fetch_hold($holdid);
859 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
861 $evt = $self->_reset_hold($reqr, $hold);
867 my ($self, $reqr, $hold) = @_;
869 my $e = new_editor(xact =>1, requestor => $reqr);
871 $logger->info("reseting hold ".$hold->id);
875 if( $hold->capture_time and $hold->current_copy ) {
877 my $copy = $e->retrieve_asset_copy($hold->current_copy)
880 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
881 $logger->info("setting copy to status 'reshelving' on hold retarget");
882 $copy->status(OILS_COPY_STATUS_RESHELVING);
883 $copy->editor($e->requestor->id);
884 $copy->edit_date('now');
885 $e->update_asset_copy($copy) or return $e->event;
887 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
889 # We don't want the copy to remain "in transit"
890 $copy->status(OILS_COPY_STATUS_RESHELVING);
891 $logger->warn("! reseting hold [$hid] that is in transit");
892 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
895 my $trans = $e->retrieve_action_transit_copy($transid);
897 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
898 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
899 $logger->info("Transit abort completed with result $evt");
900 return $evt unless "$evt" eq 1;
906 $hold->clear_capture_time;
907 $hold->clear_current_copy;
909 $e->update_action_hold_request($hold) or return $e->event;
913 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
919 __PACKAGE__->register_method(
920 method => 'fetch_open_title_holds',
921 api_name => 'open-ils.circ.open_holds.retrieve',
923 Returns a list ids of un-fulfilled holds for a given title id
924 @param authtoken The login session key
925 @param id the id of the item whose holds we want to retrieve
926 @param type The hold type - M, T, V, C
930 sub fetch_open_title_holds {
931 my( $self, $conn, $auth, $id, $type, $org ) = @_;
932 my $e = new_editor( authtoken => $auth );
933 return $e->event unless $e->checkauth;
936 $org ||= $e->requestor->ws_ou;
938 # return $e->search_action_hold_request(
939 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
941 # XXX make me return IDs in the future ^--
942 my $holds = $e->search_action_hold_request(
945 cancel_time => undef,
947 fulfillment_time => undef
951 flesh_hold_transits($holds);
956 sub flesh_hold_transits {
958 for my $hold ( @$holds ) {
960 $apputils->simplereq(
962 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
963 { hold => $hold->id },
964 { order_by => { ahtc => 'id desc' }, limit => 1 }
970 sub flesh_hold_notices {
971 my( $holds, $e ) = @_;
974 for my $hold (@$holds) {
975 my $notices = $e->search_action_hold_notification(
977 { hold => $hold->id },
978 { order_by => { anh => { 'notify_time desc' } } },
983 $hold->notify_count(scalar(@$notices));
985 my $n = $e->retrieve_action_hold_notification($$notices[0])
987 $hold->notify_time($n->notify_time);
995 __PACKAGE__->register_method(
996 method => 'fetch_captured_holds',
997 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
999 Returns a list of un-fulfilled holds for a given title id
1000 @param authtoken The login session key
1001 @param org The org id of the location in question
1005 __PACKAGE__->register_method(
1006 method => 'fetch_captured_holds',
1007 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1009 Returns a list ids of un-fulfilled holds for a given title id
1010 @param authtoken The login session key
1011 @param org The org id of the location in question
1015 sub fetch_captured_holds {
1016 my( $self, $conn, $auth, $org ) = @_;
1018 my $e = new_editor(authtoken => $auth);
1019 return $e->event unless $e->checkauth;
1020 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1022 $org ||= $e->requestor->ws_ou;
1024 my $holds = $e->search_action_hold_request(
1026 capture_time => { "!=" => undef },
1027 current_copy => { "!=" => undef },
1028 fulfillment_time => undef,
1030 cancel_time => undef,
1035 for my $h (@$holds) {
1036 my $copy = $e->retrieve_asset_copy($h->current_copy)
1037 or return $e->event;
1039 $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
1042 if( ! $self->api_name =~ /id_list/ ) {
1043 flesh_hold_transits(\@res);
1044 flesh_hold_notices(\@res, $e);
1047 if( $self->api_name =~ /id_list/ ) {
1048 return [ map { $_->id } @res ];
1055 __PACKAGE__->register_method(
1056 method => "check_title_hold",
1057 api_name => "open-ils.circ.title_hold.is_possible",
1059 Determines if a hold were to be placed by a given user,
1060 whether or not said hold would have any potential copies
1062 @param authtoken The login session key
1063 @param params A hash of named params including:
1064 patronid - the id of the hold recipient
1065 titleid (brn) - the id of the title to be held
1066 depth - the hold range depth (defaults to 0)
1069 sub check_title_hold {
1070 my( $self, $client, $authtoken, $params ) = @_;
1072 my %params = %$params;
1073 my $titleid = $params{titleid} ||"";
1074 my $volid = $params{volume_id};
1075 my $copyid = $params{copy_id};
1076 my $mrid = $params{mrid} ||"";
1077 my $depth = $params{depth} || 0;
1078 my $pickup_lib = $params{pickup_lib};
1079 my $hold_type = $params{hold_type} || 'T';
1081 my $e = new_editor(authtoken=>$authtoken);
1082 return $e->event unless $e->checkauth;
1083 my $patron = $e->retrieve_actor_user($params{patronid})
1084 or return $e->event;
1086 if( $e->requestor->id ne $patron->id ) {
1087 return $e->event unless
1088 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1091 return OpenILS::Event->new('PATRON_BARRED')
1092 if $patron->barred and
1093 ($patron->barred =~ /t/i or $patron->barred == 1);
1095 my $rangelib = $params{range_lib} || $patron->home_ou;
1097 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1098 or return $e->event;
1100 $logger->info("checking hold possibility with type $hold_type");
1106 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1108 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1109 $volume = $e->retrieve_asset_call_number($copy->call_number)
1110 or return $e->event;
1111 $title = $e->retrieve_biblio_record_entry($volume->record)
1112 or return $e->event;
1113 return verify_copy_for_hold(
1114 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1116 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1118 $volume = $e->retrieve_asset_call_number($volid)
1119 or return $e->event;
1120 $title = $e->retrieve_biblio_record_entry($volume->record)
1121 or return $e->event;
1123 return _check_volume_hold_is_possible(
1124 $volume, $title, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1126 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1128 return _check_title_hold_is_possible(
1129 $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1131 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1133 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1134 my @recs = map { $_->source } @$maps;
1135 for my $rec (@recs) {
1136 return 1 if (_check_title_hold_is_possible(
1137 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
1145 sub _check_title_hold_is_possible {
1146 my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1152 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1154 while( $title = $U->storagereq(
1155 'open-ils.storage.biblio.record_entry.ranged_tree',
1156 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1160 ref($title->call_numbers) and
1161 @{$title->call_numbers};
1163 for my $cn (@{$title->call_numbers}) {
1165 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1167 for my $copy (@{$cn->copies}) {
1168 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1169 return 1 if verify_copy_for_hold(
1170 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1171 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1180 sub _check_volume_hold_is_possible {
1181 my( $vol, $title, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1182 my $copies = new_editor->search_asset_copy({call_number => $vol->id});
1183 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1184 for my $copy ( @$copies ) {
1185 return 1 if verify_copy_for_hold(
1186 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1193 sub verify_copy_for_hold {
1194 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1195 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1196 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1197 { patron => $patron,
1198 requestor => $requestor,
1201 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1202 pickup_lib => $pickup_lib,
1203 request_lib => $request_lib
1211 sub find_nearest_permitted_hold {
1214 my $session = shift;
1217 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1219 # first see if this copy has already been selected to fulfill a hold
1220 my $hold = $session->request(
1221 "open-ils.storage.direct.action.hold_request.search_where",
1222 { current_copy => $copy->id, cancel_time => undef, capture_time => undef } )->gather(1);
1225 $logger->info("hold found which can be fulfilled by copy ".$copy->id);
1229 # We know this hold is permitted, so just return it
1230 return $hold if $hold;
1232 $logger->debug("searching for potential holds at org ".
1233 $user->ws_ou." and copy ".$copy->id);
1235 my $holds = $session->request(
1236 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1237 $user->ws_ou, $copy->id, 5 )->gather(1);
1239 return (undef, $evt) unless @$holds;
1241 # for each potential hold, we have to run the permit script
1242 # to make sure the hold is actually permitted.
1244 for my $holdid (@$holds) {
1245 next unless $holdid;
1246 $logger->info("Checking if hold $holdid is permitted for user ".$user->id);
1248 my ($hold) = $U->fetch_hold($holdid);
1250 my ($reqr) = $U->fetch_user($hold->requestor);
1252 my ($rlib) = $U->fetch_org_unit($hold->request_lib);
1254 return ($hold) if OpenILS::Utils::PermitHold::permit_copy_hold(
1256 patron_id => $hold->usr,
1257 requestor => $reqr->id,
1259 pickup_lib => $hold->pickup_lib,
1260 request_lib => $rlib,
1265 return (undef, $evt);
1273 __PACKAGE__->register_method(
1274 method => 'all_rec_holds',
1275 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1279 my( $self, $conn, $auth, $title_id, $args ) = @_;
1281 my $e = new_editor(authtoken=>$auth);
1282 $e->checkauth or return $e->event;
1283 $e->allowed('VIEW_HOLD') or return $e->event;
1285 $args ||= { fulfillment_time => undef };
1286 $args->{cancel_time} = undef;
1288 my $resp = { volume_holds => [], copy_holds => [] };
1290 $resp->{title_holds} = $e->search_action_hold_request(
1292 hold_type => OILS_HOLD_TYPE_TITLE,
1293 target => $title_id,
1297 my $vols = $e->search_asset_call_number(
1298 { record => $title_id, deleted => 'f' }, {idlist=>1});
1300 return $resp unless @$vols;
1302 $resp->{volume_holds} = $e->search_action_hold_request(
1304 hold_type => OILS_HOLD_TYPE_VOLUME,
1309 my $copies = $e->search_asset_copy(
1310 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1312 return $resp unless @$copies;
1314 $resp->{copy_holds} = $e->search_action_hold_request(
1316 hold_type => OILS_HOLD_TYPE_COPY,
1328 __PACKAGE__->register_method(
1329 method => 'uber_hold',
1330 api_name => 'open-ils.circ.hold.details.retrieve'
1334 my($self, $client, $auth, $hold_id) = @_;
1335 my $e = new_editor(authtoken=>$auth);
1336 $e->checkauth or return $e->event;
1337 $e->allowed('VIEW_HOLD') or return $e->event;
1341 my $hold = $e->retrieve_action_hold_request(
1346 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1349 ) or return $e->event;
1351 my $user = $hold->usr;
1352 $hold->usr($user->id);
1354 my $card = $e->retrieve_actor_card($user->card)
1355 or return $e->event;
1357 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1359 flesh_hold_notices([$hold], $e);
1360 flesh_hold_transits([$hold]);
1367 status => _hold_status($e, $hold),
1368 patron_first => $user->first_given_name,
1369 patron_last => $user->family_name,
1370 patron_barcode => $card->barcode,
1376 # -----------------------------------------------------
1377 # Returns the MVR object that represents what the
1379 # -----------------------------------------------------
1381 my( $e, $hold ) = @_;
1387 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1388 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1389 or return $e->event;
1390 $tid = $mr->master_record;
1392 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1393 $tid = $hold->target;
1395 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1396 $volume = $e->retrieve_asset_call_number($hold->target)
1397 or return $e->event;
1398 $tid = $volume->record;
1400 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1401 $copy = $e->retrieve_asset_copy($hold->target)
1402 or return $e->event;
1403 $volume = $e->retrieve_asset_call_number($copy->call_number)
1404 or return $e->event;
1405 $tid = $volume->record;
1408 if(!$copy and ref $hold->current_copy ) {
1409 $copy = $hold->current_copy;
1410 $hold->current_copy($copy->id);
1413 if(!$volume and $copy) {
1414 $volume = $e->retrieve_asset_call_number($copy->call_number);
1417 my $title = $e->retrieve_biblio_record_entry($tid);
1418 return ( $U->record_to_mvr($title), $volume, $copy );