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 # Go ahead and target the copy-level holds
152 'open-ils.storage.action.hold_request.copy_targeter',
153 undef, $_->id ) for @copyholds;
159 my( $self, $client, $login_session, @holds) = @_;
161 if(!@holds){return 0;}
162 my( $user, $evt ) = $apputils->checkses($login_session);
166 if(ref($holds[0]) eq 'ARRAY') {
168 } else { $holds = [ @holds ]; }
170 $logger->debug("Iterating over holds requests...");
172 for my $hold (@$holds) {
175 my $type = $hold->hold_type;
177 $logger->activity("User " . $user->id .
178 " creating new hold of type $type for user " . $hold->usr);
181 if($user->id ne $hold->usr) {
182 ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
192 # am I allowed to place holds for this user?
193 if($hold->requestor ne $hold->usr) {
194 $perm = _check_request_holds_perm($user->id, $user->home_ou);
195 if($perm) { return $perm; }
198 # is this user allowed to have holds of this type?
199 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
201 #if there is a requestor, see if the requestor has override privelages
202 if($hold->requestor ne $hold->usr) {
203 $perm = _check_request_holds_override($user->id, $user->home_ou);
204 if($perm) {return $perm;}
211 #enforce the fact that the login is the one requesting the hold
212 $hold->requestor($user->id);
213 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
215 my $resp = $apputils->simplereq(
217 'open-ils.storage.direct.action.hold_request.create', $hold );
220 return OpenSRF::EX::ERROR ("Error creating hold");
227 # makes sure that a user has permission to place the type of requested hold
228 # returns the Perm exception if not allowed, returns undef if all is well
229 sub _check_holds_perm {
230 my($type, $user_id, $org_id) = @_;
234 if($evt = $apputils->check_perms(
235 $user_id, $org_id, "MR_HOLDS")) {
239 } elsif ($type eq "T") {
240 if($evt = $apputils->check_perms(
241 $user_id, $org_id, "TITLE_HOLDS")) {
245 } elsif($type eq "V") {
246 if($evt = $apputils->check_perms(
247 $user_id, $org_id, "VOLUME_HOLDS")) {
251 } elsif($type eq "C") {
252 if($evt = $apputils->check_perms(
253 $user_id, $org_id, "COPY_HOLDS")) {
261 # tests if the given user is allowed to place holds on another's behalf
262 sub _check_request_holds_perm {
265 if(my $evt = $apputils->check_perms(
266 $user_id, $org_id, "REQUEST_HOLDS")) {
271 sub _check_request_holds_override {
274 if(my $evt = $apputils->check_perms(
275 $user_id, $org_id, "REQUEST_HOLDS_OVERRIDE")) {
280 __PACKAGE__->register_method(
281 method => "retrieve_holds_by_id",
282 api_name => "open-ils.circ.holds.retrieve_by_id",
284 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
285 different from the user, then the requestor must have VIEW_HOLD permissions.
289 sub retrieve_holds_by_id {
290 my($self, $client, $auth, $hold_id) = @_;
291 my $e = new_editor(authtoken=>$auth);
292 $e->checkauth or return $e->event;
293 $e->allowed('VIEW_HOLD') or return $e->event;
295 my $holds = $e->search_action_hold_request(
297 { id => $hold_id , fulfillment_time => undef },
298 { order_by => { ahr => "request_time" } }
302 flesh_hold_transits($holds);
303 flesh_hold_notices($holds, $e);
308 __PACKAGE__->register_method(
309 method => "retrieve_holds",
310 api_name => "open-ils.circ.holds.retrieve",
312 Retrieves all the holds, with hold transits attached, for the specified
313 user id. The login session is the requestor and if the requestor is
314 different from the user, then the requestor must have VIEW_HOLD permissions.
317 __PACKAGE__->register_method(
318 method => "retrieve_holds",
319 api_name => "open-ils.circ.holds.id_list.retrieve",
321 Retrieves all the hold ids for the specified
322 user id. The login session is the requestor and if the requestor is
323 different from the user, then the requestor must have VIEW_HOLD permissions.
327 my($self, $client, $login_session, $user_id) = @_;
329 my( $user, $target, $evt ) = $apputils->checkses_requestor(
330 $login_session, $user_id, 'VIEW_HOLD' );
333 my $holds = $apputils->simplereq(
335 "open-ils.cstore.direct.action.hold_request.search.atomic",
338 fulfillment_time => undef,
339 cancel_time => undef,
341 { order_by => { ahr => "request_time" } }
344 if( ! $self->api_name =~ /id_list/ ) {
345 for my $hold ( @$holds ) {
347 $apputils->simplereq(
349 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
350 { hold => $hold->id },
351 { order_by => { ahtc => 'id desc' }, limit => 1 }
357 if( $self->api_name =~ /id_list/ ) {
358 return [ map { $_->id } @$holds ];
364 __PACKAGE__->register_method(
365 method => "retrieve_holds_by_pickup_lib",
366 api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
368 Retrieves all the holds, with hold transits attached, for the specified
372 __PACKAGE__->register_method(
373 method => "retrieve_holds_by_pickup_lib",
374 api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
376 Retrieves all the hold ids for the specified
380 sub retrieve_holds_by_pickup_lib {
381 my($self, $client, $login_session, $ou_id) = @_;
383 #FIXME -- put an appropriate permission check here
384 #my( $user, $target, $evt ) = $apputils->checkses_requestor(
385 # $login_session, $user_id, 'VIEW_HOLD' );
386 #return $evt if $evt;
388 my $holds = $apputils->simplereq(
390 "open-ils.cstore.direct.action.hold_request.search.atomic",
392 pickup_lib => $ou_id ,
393 fulfillment_time => undef,
396 { order_by => { ahr => "request_time" } });
399 if( ! $self->api_name =~ /id_list/ ) {
400 flesh_hold_transits($holds);
403 if( $self->api_name =~ /id_list/ ) {
404 return [ map { $_->id } @$holds ];
410 __PACKAGE__->register_method(
411 method => "cancel_hold",
412 api_name => "open-ils.circ.hold.cancel",
414 Cancels the specified hold. The login session
415 is the requestor and if the requestor is different from the usr field
416 on the hold, the requestor must have CANCEL_HOLDS permissions.
417 the hold may be either the hold object or the hold id
421 my($self, $client, $auth, $holdid) = @_;
423 my $e = new_editor(authtoken=>$auth, xact=>1);
424 return $e->event unless $e->checkauth;
426 my $hold = $e->retrieve_action_hold_request($holdid)
429 if( $e->requestor->id ne $hold->usr ) {
430 return $e->event unless $e->allowed('CANCEL_HOLDS');
433 return 1 if $hold->cancel_time;
435 # If the hold is captured, reset the copy status
436 if( $hold->capture_time and $hold->current_copy ) {
438 my $copy = $e->retrieve_asset_copy($hold->current_copy)
441 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
442 $logger->info("setting copy to status 'reshelving' on hold cancel");
443 $copy->status(OILS_COPY_STATUS_RESHELVING);
444 $copy->editor($e->requestor->id);
445 $copy->edit_date('now');
446 $e->update_asset_copy($copy) or return $e->event;
448 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
451 # We don't want the copy to remain "in transit"
452 $copy->status(OILS_COPY_STATUS_RESHELVING);
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;
468 $hold->cancel_time('now');
469 $e->update_action_hold_request($hold)
472 $self->delete_hold_copy_maps($e, $hold->id);
478 sub delete_hold_copy_maps {
483 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
485 $editor->delete_action_hold_copy_map($_)
486 or return $editor->event;
492 __PACKAGE__->register_method(
493 method => "update_hold",
494 api_name => "open-ils.circ.hold.update",
496 Updates the specified hold. The login session
497 is the requestor and if the requestor is different from the usr field
498 on the hold, the requestor must have UPDATE_HOLDS permissions.
502 my($self, $client, $login_session, $hold) = @_;
504 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
505 $login_session, $hold->usr, 'UPDATE_HOLD' );
508 $logger->activity('User ' . $requestor->id .
509 ' updating hold ' . $hold->id . ' for user ' . $target->id );
511 return $U->storagereq(
512 "open-ils.storage.direct.action.hold_request.update", $hold );
516 __PACKAGE__->register_method(
517 method => "retrieve_hold_status",
518 api_name => "open-ils.circ.hold.status.retrieve",
520 Calculates the current status of the hold.
521 the requestor must have VIEW_HOLD permissions if the hold is for a user
522 other than the requestor.
523 Returns -1 on error (for now)
524 Returns 1 for 'waiting for copy to become available'
525 Returns 2 for 'waiting for copy capture'
526 Returns 3 for 'in transit'
527 Returns 4 for 'arrived'
530 sub retrieve_hold_status {
531 my($self, $client, $auth, $hold_id) = @_;
533 my $e = new_editor(authtoken => $auth);
534 return $e->event unless $e->checkauth;
535 my $hold = $e->retrieve_action_hold_request($hold_id)
538 if( $e->requestor->id != $hold->usr ) {
539 return $e->event unless $e->allowed('VIEW_HOLD');
542 return _hold_status($e, $hold);
548 return 1 unless $hold->current_copy;
549 return 2 unless $hold->capture_time;
551 my $copy = $e->retrieve_asset_copy($hold->current_copy)
554 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
555 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
565 __PACKAGE__->register_method(
566 method => "capture_copy",
567 api_name => "open-ils.circ.hold.capture_copy.barcode",
569 Captures a copy to fulfil a hold
570 Params is login session and copy barcode
571 Optional param is 'flesh'. If set, we also return the
572 relevant copy and title
573 login mus have COPY_CHECKIN permissions (since this is essentially
577 # XXX deprecate me XXX
580 my( $self, $client, $login_session, $params ) = @_;
581 my %params = %$params;
582 my $barcode = $params{barcode};
585 my( $user, $target, $copy, $hold, $evt );
587 ( $user, $evt ) = $apputils->checkses($login_session);
590 # am I allowed to checkin a copy?
591 $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
594 $logger->info("Capturing copy with barcode $barcode");
596 my $session = $apputils->start_db_session();
598 ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
601 $logger->debug("Capturing copy " . $copy->id);
603 #( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
604 ( $hold, $evt ) = $self->find_nearest_permitted_hold($session, $copy, $user);
607 warn "Found hold " . $hold->id . "\n";
608 $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
610 $hold->current_copy($copy->id);
611 $hold->capture_time("now");
614 my $stat = $session->request(
615 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
616 if(!$stat) { throw OpenSRF::EX::ERROR
617 ("Error updating hold request " . $copy->id); }
619 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF); #status on holds shelf
621 # if the staff member capturing this item is not at the pickup lib
622 if( $user->home_ou ne $hold->pickup_lib ) {
623 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
626 $copy->editor($user->id);
627 $copy->edit_date("now");
628 $stat = $session->request(
629 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
630 if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
632 my $payload = { hold => $hold };
633 $payload->{copy} = $copy if $params{flesh_copy};
635 if($params{flesh_record}) {
637 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
639 $record = $apputils->record_to_mvr($record);
640 $payload->{record} = $record;
643 $apputils->commit_db_session($session);
645 return OpenILS::Event->new('ROUTE_ITEM',
646 route_to => $hold->pickup_lib, payload => $payload );
649 sub _build_hold_transit {
650 my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
651 my $trans = Fieldmapper::action::hold_transit_copy->new;
653 $trans->hold($hold->id);
654 $trans->source($user->home_ou);
655 $trans->dest($hold->pickup_lib);
656 $trans->source_send_time("now");
657 $trans->target_copy($copy->id);
658 $trans->copy_status($copy->status);
660 my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
661 my ($stat) = $meth->run( $login_session, $trans, $session );
662 if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
663 else { $copy->status(6); } #status in transit
668 __PACKAGE__->register_method(
669 method => "create_hold_transit",
670 api_name => "open-ils.circ.hold_transit.create",
672 Creates a new transit object
675 sub create_hold_transit {
676 my( $self, $client, $login_session, $transit, $session ) = @_;
678 my( $user, $evt ) = $apputils->checkses($login_session);
680 $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
684 if($session) { $ses = $session; }
685 else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
687 return $ses->request(
688 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
694 sub find_local_hold {
695 my( $class, $session, $copy, $user ) = @_;
696 return $class->find_nearest_permitted_hold($session, $copy, $user);
704 sub fetch_open_hold_by_current_copy {
707 my $hold = $apputils->simplereq(
709 'open-ils.cstore.direct.action.hold_request.search.atomic',
710 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
711 return $hold->[0] if ref($hold);
715 sub fetch_related_holds {
718 return $apputils->simplereq(
720 'open-ils.cstore.direct.action.hold_request.search.atomic',
721 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
725 __PACKAGE__->register_method (
726 method => "hold_pull_list",
727 api_name => "open-ils.circ.hold_pull_list.retrieve",
729 Returns a list of holds that need to be "pulled"
734 __PACKAGE__->register_method (
735 method => "hold_pull_list",
736 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
738 Returns a list of hold ID's that need to be "pulled"
745 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
746 my( $reqr, $evt ) = $U->checkses($authtoken);
749 my $org = $reqr->ws_ou || $reqr->home_ou;
750 # the perm locaiton shouldn't really matter here since holds
751 # will exist all over and VIEW_HOLDS should be universal
752 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
755 if( $self->api_name =~ /id_list/ ) {
756 return $U->storagereq(
757 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
758 $org, $limit, $offset );
760 return $U->storagereq(
761 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
762 $org, $limit, $offset );
766 __PACKAGE__->register_method (
767 method => 'fetch_hold_notify',
768 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
770 Returns a list of hold notification objects based on hold id.
771 @param authtoken The loggin session key
772 @param holdid The id of the hold whose notifications we want to retrieve
773 @return An array of hold notification objects, event on error.
777 sub fetch_hold_notify {
778 my( $self, $conn, $authtoken, $holdid ) = @_;
779 my( $requestor, $evt ) = $U->checkses($authtoken);
782 ($hold, $evt) = $U->fetch_hold($holdid);
784 ($patron, $evt) = $U->fetch_user($hold->usr);
787 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
790 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
791 return $U->cstorereq(
792 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
796 __PACKAGE__->register_method (
797 method => 'create_hold_notify',
798 api_name => 'open-ils.circ.hold_notification.create',
800 Creates a new hold notification object
801 @param authtoken The login session key
802 @param notification The hold notification object to create
803 @return ID of the new object on success, Event on error
806 sub create_hold_notify {
807 my( $self, $conn, $authtoken, $notification ) = @_;
808 my( $requestor, $evt ) = $U->checkses($authtoken);
811 ($hold, $evt) = $U->fetch_hold($notification->hold);
813 ($patron, $evt) = $U->fetch_user($hold->usr);
816 # XXX perm depth probably doesn't matter here -- should always be consortium level
817 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
820 # Set the proper notifier
821 $notification->notify_staff($requestor->id);
822 my $id = $U->storagereq(
823 'open-ils.storage.direct.action.hold_notification.create', $notification );
824 return $U->DB_UPDATE_FAILED($notification) unless $id;
825 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
830 __PACKAGE__->register_method(
831 method => 'reset_hold',
832 api_name => 'open-ils.circ.hold.reset',
834 Un-captures and un-targets a hold, essentially returning
835 it to the state it was in directly after it was placed,
836 then attempts to re-target the hold
837 @param authtoken The login session key
838 @param holdid The id of the hold
844 my( $self, $conn, $auth, $holdid ) = @_;
846 my ($hold, $evt) = $U->fetch_hold($holdid);
848 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
850 $evt = $self->_reset_hold($reqr, $hold);
856 my ($self, $reqr, $hold) = @_;
858 my $e = new_editor(xact =>1, requestor => $reqr);
860 $logger->info("reseting hold ".$hold->id);
864 if( $hold->capture_time and $hold->current_copy ) {
866 my $copy = $e->retrieve_asset_copy($hold->current_copy)
869 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
870 $logger->info("setting copy to status 'reshelving' on hold retarget");
871 $copy->status(OILS_COPY_STATUS_RESHELVING);
872 $copy->editor($e->requestor->id);
873 $copy->edit_date('now');
874 $e->update_asset_copy($copy) or return $e->event;
876 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
878 # We don't want the copy to remain "in transit"
879 $copy->status(OILS_COPY_STATUS_RESHELVING);
880 $logger->warn("! reseting hold [$hid] that is in transit");
881 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
884 my $trans = $e->retrieve_action_transit_copy($transid);
886 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
887 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
888 $logger->info("Transit abort completed with result $evt");
889 return $evt unless "$evt" eq 1;
895 $hold->clear_capture_time;
896 $hold->clear_current_copy;
898 $e->update_action_hold_request($hold) or return $e->event;
902 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
908 __PACKAGE__->register_method(
909 method => 'fetch_open_title_holds',
910 api_name => 'open-ils.circ.open_holds.retrieve',
912 Returns a list ids of un-fulfilled holds for a given title id
913 @param authtoken The login session key
914 @param id the id of the item whose holds we want to retrieve
915 @param type The hold type - M, T, V, C
919 sub fetch_open_title_holds {
920 my( $self, $conn, $auth, $id, $type, $org ) = @_;
921 my $e = new_editor( authtoken => $auth );
922 return $e->event unless $e->checkauth;
925 $org ||= $e->requestor->ws_ou;
927 # return $e->search_action_hold_request(
928 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
930 # XXX make me return IDs in the future ^--
931 my $holds = $e->search_action_hold_request(
934 cancel_time => undef,
936 fulfillment_time => undef
940 flesh_hold_transits($holds);
945 sub flesh_hold_transits {
947 for my $hold ( @$holds ) {
949 $apputils->simplereq(
951 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
952 { hold => $hold->id },
953 { order_by => { ahtc => 'id desc' }, limit => 1 }
959 sub flesh_hold_notices {
960 my( $holds, $e ) = @_;
963 for my $hold (@$holds) {
964 my $notices = $e->search_action_hold_notification(
966 { hold => $hold->id },
967 { order_by => { anh => { 'notify_time desc' } } },
972 $hold->notify_count(scalar(@$notices));
974 my $n = $e->retrieve_action_hold_notification($$notices[0])
976 $hold->notify_time($n->notify_time);
984 __PACKAGE__->register_method(
985 method => 'fetch_captured_holds',
986 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
988 Returns a list of un-fulfilled holds for a given title id
989 @param authtoken The login session key
990 @param org The org id of the location in question
994 __PACKAGE__->register_method(
995 method => 'fetch_captured_holds',
996 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
998 Returns a list ids of un-fulfilled holds for a given title id
999 @param authtoken The login session key
1000 @param org The org id of the location in question
1004 sub fetch_captured_holds {
1005 my( $self, $conn, $auth, $org ) = @_;
1007 my $e = new_editor(authtoken => $auth);
1008 return $e->event unless $e->checkauth;
1009 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1011 $org ||= $e->requestor->ws_ou;
1013 my $holds = $e->search_action_hold_request(
1015 capture_time => { "!=" => undef },
1016 current_copy => { "!=" => undef },
1017 fulfillment_time => undef,
1019 cancel_time => undef,
1024 for my $h (@$holds) {
1025 my $copy = $e->retrieve_asset_copy($h->current_copy)
1026 or return $e->event;
1028 $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
1031 if( ! $self->api_name =~ /id_list/ ) {
1032 flesh_hold_transits(\@res);
1033 flesh_hold_notices(\@res, $e);
1036 if( $self->api_name =~ /id_list/ ) {
1037 return [ map { $_->id } @res ];
1044 __PACKAGE__->register_method(
1045 method => "check_title_hold",
1046 api_name => "open-ils.circ.title_hold.is_possible",
1048 Determines if a hold were to be placed by a given user,
1049 whether or not said hold would have any potential copies
1051 @param authtoken The login session key
1052 @param params A hash of named params including:
1053 patronid - the id of the hold recipient
1054 titleid (brn) - the id of the title to be held
1055 depth - the hold range depth (defaults to 0)
1058 sub check_title_hold {
1059 my( $self, $client, $authtoken, $params ) = @_;
1061 my %params = %$params;
1062 my $titleid = $params{titleid} ||"";
1063 my $volid = $params{volume_id};
1064 my $copyid = $params{copy_id};
1065 my $mrid = $params{mrid} ||"";
1066 my $depth = $params{depth} || 0;
1067 my $pickup_lib = $params{pickup_lib};
1068 my $hold_type = $params{hold_type} || 'T';
1070 my $e = new_editor(authtoken=>$authtoken);
1071 return $e->event unless $e->checkauth;
1072 my $patron = $e->retrieve_actor_user($params{patronid})
1073 or return $e->event;
1075 if( $e->requestor->id ne $patron->id ) {
1076 return $e->event unless
1077 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1080 return OpenILS::Event->new('PATRON_BARRED')
1081 if $patron->barred and
1082 ($patron->barred =~ /t/i or $patron->barred == 1);
1084 my $rangelib = $params{range_lib} || $patron->home_ou;
1086 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1087 or return $e->event;
1089 $logger->info("checking hold possibility with type $hold_type");
1095 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1097 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1098 $volume = $e->retrieve_asset_call_number($copy->call_number)
1099 or return $e->event;
1100 $title = $e->retrieve_biblio_record_entry($volume->record)
1101 or return $e->event;
1102 return verify_copy_for_hold(
1103 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1105 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1107 $volume = $e->retrieve_asset_call_number($volid)
1108 or return $e->event;
1109 $title = $e->retrieve_biblio_record_entry($volume->record)
1110 or return $e->event;
1112 return _check_volume_hold_is_possible(
1113 $volume, $title, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1115 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1117 return _check_title_hold_is_possible(
1118 $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1120 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1122 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1123 my @recs = map { $_->source } @$maps;
1124 for my $rec (@recs) {
1125 return 1 if (_check_title_hold_is_possible(
1126 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
1134 sub _check_title_hold_is_possible {
1135 my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1141 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1143 while( $title = $U->storagereq(
1144 'open-ils.storage.biblio.record_entry.ranged_tree',
1145 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1149 ref($title->call_numbers) and
1150 @{$title->call_numbers};
1152 for my $cn (@{$title->call_numbers}) {
1154 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1156 for my $copy (@{$cn->copies}) {
1157 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1158 return 1 if verify_copy_for_hold(
1159 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1160 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1169 sub _check_volume_hold_is_possible {
1170 my( $vol, $title, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1171 my $copies = new_editor->search_asset_copy({call_number => $vol->id});
1172 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1173 for my $copy ( @$copies ) {
1174 return 1 if verify_copy_for_hold(
1175 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1182 sub verify_copy_for_hold {
1183 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1184 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1185 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1186 { patron => $patron,
1187 requestor => $requestor,
1190 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1191 pickup_lib => $pickup_lib,
1192 request_lib => $request_lib
1200 sub find_nearest_permitted_hold {
1203 my $session = shift;
1206 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1208 # first see if this copy has already been selected to fulfill a hold
1209 my $hold = $session->request(
1210 "open-ils.storage.direct.action.hold_request.search_where",
1211 { current_copy => $copy->id, cancel_time => undef, capture_time => undef } )->gather(1);
1214 $logger->info("hold found which can be fulfilled by copy ".$copy->id);
1218 # We know this hold is permitted, so just return it
1219 return $hold if $hold;
1221 $logger->debug("searching for potential holds at org ".
1222 $user->ws_ou." and copy ".$copy->id);
1224 my $holds = $session->request(
1225 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1226 $user->ws_ou, $copy->id, 5 )->gather(1);
1228 return (undef, $evt) unless @$holds;
1230 # for each potential hold, we have to run the permit script
1231 # to make sure the hold is actually permitted.
1233 for my $holdid (@$holds) {
1234 next unless $holdid;
1235 $logger->info("Checking if hold $holdid is permitted for user ".$user->id);
1237 my ($hold) = $U->fetch_hold($holdid);
1239 my ($reqr) = $U->fetch_user($hold->requestor);
1241 return ($hold) if OpenILS::Utils::PermitHold::permit_copy_hold(
1243 patron_id => $hold->usr,
1244 requestor => $reqr->id,
1246 pickup_lib => $hold->pickup_lib,
1247 request_lib => $hold->request_lib
1252 return (undef, $evt);
1260 __PACKAGE__->register_method(
1261 method => 'all_rec_holds',
1262 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1266 my( $self, $conn, $auth, $title_id, $args ) = @_;
1268 my $e = new_editor(authtoken=>$auth);
1269 $e->checkauth or return $e->event;
1270 $e->allowed('VIEW_HOLD') or return $e->event;
1272 $args ||= { fulfillment_time => undef };
1273 $args->{cancel_time} = undef;
1275 my $resp = { volume_holds => [], copy_holds => [] };
1277 $resp->{title_holds} = $e->search_action_hold_request(
1279 hold_type => OILS_HOLD_TYPE_TITLE,
1280 target => $title_id,
1284 my $vols = $e->search_asset_call_number(
1285 { record => $title_id, deleted => 'f' }, {idlist=>1});
1287 return $resp unless @$vols;
1289 $resp->{volume_holds} = $e->search_action_hold_request(
1291 hold_type => OILS_HOLD_TYPE_VOLUME,
1296 my $copies = $e->search_asset_copy(
1297 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1299 return $resp unless @$copies;
1301 $resp->{copy_holds} = $e->search_action_hold_request(
1303 hold_type => OILS_HOLD_TYPE_COPY,
1315 __PACKAGE__->register_method(
1316 method => 'uber_hold',
1317 api_name => 'open-ils.circ.hold.details.retrieve'
1321 my($self, $client, $auth, $hold_id) = @_;
1322 my $e = new_editor(authtoken=>$auth);
1323 $e->checkauth or return $e->event;
1324 $e->allowed('VIEW_HOLD') or return $e->event;
1328 my $hold = $e->retrieve_action_hold_request(
1333 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1336 ) or return $e->event;
1338 my $user = $hold->usr;
1339 $hold->usr($user->id);
1341 my $card = $e->retrieve_actor_card($user->card)
1342 or return $e->event;
1344 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1346 flesh_hold_notices([$hold], $e);
1347 flesh_hold_transits([$hold]);
1354 status => _hold_status($e, $hold),
1355 patron_first => $user->first_given_name,
1356 patron_last => $user->family_name,
1357 patron_barcode => $card->barcode,
1363 # -----------------------------------------------------
1364 # Returns the MVR object that represents what the
1366 # -----------------------------------------------------
1368 my( $e, $hold ) = @_;
1374 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1375 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1376 or return $e->event;
1377 $tid = $mr->master_record;
1379 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1380 $tid = $hold->target;
1382 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1383 $volume = $e->retrieve_asset_call_number($hold->target)
1384 or return $e->event;
1385 $tid = $volume->record;
1387 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1388 $copy = $e->retrieve_asset_copy($hold->target)
1389 or return $e->event;
1390 $volume = $e->retrieve_asset_call_number($copy->call_number)
1391 or return $e->event;
1392 $tid = $volume->record;
1395 if(!$copy and ref $hold->current_copy ) {
1396 $copy = $hold->current_copy;
1397 $hold->current_copy($copy->id);
1400 if(!$volume and $copy) {
1401 $volume = $e->retrieve_asset_call_number($copy->call_number);
1404 my $title = $e->retrieve_biblio_record_entry($tid);
1405 return ( $U->record_to_mvr($title), $volume, $copy );