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 $logger->warn("! canceling hold [$hid] that is in transit");
452 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
455 my $trans = $e->retrieve_action_transit_copy($transid);
457 $logger->info("Aborting transit [$transid] on hold [$hid] cancel...");
458 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
459 $logger->info("Transit abort completed with result $evt");
460 return $evt unless "$evt" eq 1;
464 # We don't want the copy to remain "in transit" or to recover
465 # any previous statuses
466 $logger->info("setting copy back to reshelving in hold+transit cancel");
467 $copy->status(OILS_COPY_STATUS_RESHELVING);
468 $copy->editor($e->requestor->id);
469 $copy->edit_date('now');
470 $e->update_asset_copy($copy) or return $e->event;
474 $hold->cancel_time('now');
475 $e->update_action_hold_request($hold)
478 $self->delete_hold_copy_maps($e, $hold->id);
484 sub delete_hold_copy_maps {
489 my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
491 $editor->delete_action_hold_copy_map($_)
492 or return $editor->event;
498 __PACKAGE__->register_method(
499 method => "update_hold",
500 api_name => "open-ils.circ.hold.update",
502 Updates the specified hold. The login session
503 is the requestor and if the requestor is different from the usr field
504 on the hold, the requestor must have UPDATE_HOLDS permissions.
508 my($self, $client, $login_session, $hold) = @_;
510 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
511 $login_session, $hold->usr, 'UPDATE_HOLD' );
514 $logger->activity('User ' . $requestor->id .
515 ' updating hold ' . $hold->id . ' for user ' . $target->id );
517 return $U->storagereq(
518 "open-ils.storage.direct.action.hold_request.update", $hold );
522 __PACKAGE__->register_method(
523 method => "retrieve_hold_status",
524 api_name => "open-ils.circ.hold.status.retrieve",
526 Calculates the current status of the hold.
527 the requestor must have VIEW_HOLD permissions if the hold is for a user
528 other than the requestor.
529 Returns -1 on error (for now)
530 Returns 1 for 'waiting for copy to become available'
531 Returns 2 for 'waiting for copy capture'
532 Returns 3 for 'in transit'
533 Returns 4 for 'arrived'
536 sub retrieve_hold_status {
537 my($self, $client, $auth, $hold_id) = @_;
539 my $e = new_editor(authtoken => $auth);
540 return $e->event unless $e->checkauth;
541 my $hold = $e->retrieve_action_hold_request($hold_id)
544 if( $e->requestor->id != $hold->usr ) {
545 return $e->event unless $e->allowed('VIEW_HOLD');
548 return _hold_status($e, $hold);
554 return 1 unless $hold->current_copy;
555 return 2 unless $hold->capture_time;
557 my $copy = $hold->current_copy;
558 unless( ref $copy ) {
559 $copy = $e->retrieve_asset_copy($hold->current_copy)
563 return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
564 return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
574 __PACKAGE__->register_method(
575 method => "capture_copy",
576 api_name => "open-ils.circ.hold.capture_copy.barcode",
578 Captures a copy to fulfil a hold
579 Params is login session and copy barcode
580 Optional param is 'flesh'. If set, we also return the
581 relevant copy and title
582 login mus have COPY_CHECKIN permissions (since this is essentially
586 # XXX deprecate me XXX
589 my( $self, $client, $login_session, $params ) = @_;
590 my %params = %$params;
591 my $barcode = $params{barcode};
594 my( $user, $target, $copy, $hold, $evt );
596 ( $user, $evt ) = $apputils->checkses($login_session);
599 # am I allowed to checkin a copy?
600 $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
603 $logger->info("Capturing copy with barcode $barcode");
605 my $session = $apputils->start_db_session();
607 ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
610 $logger->debug("Capturing copy " . $copy->id);
612 #( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
613 ( $hold, $evt ) = $self->find_nearest_permitted_hold($session, $copy, $user);
616 warn "Found hold " . $hold->id . "\n";
617 $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
619 $hold->current_copy($copy->id);
620 $hold->capture_time("now");
623 my $stat = $session->request(
624 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
625 if(!$stat) { throw OpenSRF::EX::ERROR
626 ("Error updating hold request " . $copy->id); }
628 $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF); #status on holds shelf
630 # if the staff member capturing this item is not at the pickup lib
631 if( $user->home_ou ne $hold->pickup_lib ) {
632 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
635 $copy->editor($user->id);
636 $copy->edit_date("now");
637 $stat = $session->request(
638 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
639 if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
641 my $payload = { hold => $hold };
642 $payload->{copy} = $copy if $params{flesh_copy};
644 if($params{flesh_record}) {
646 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
648 $record = $apputils->record_to_mvr($record);
649 $payload->{record} = $record;
652 $apputils->commit_db_session($session);
654 return OpenILS::Event->new('ROUTE_ITEM',
655 route_to => $hold->pickup_lib, payload => $payload );
658 sub _build_hold_transit {
659 my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
660 my $trans = Fieldmapper::action::hold_transit_copy->new;
662 $trans->hold($hold->id);
663 $trans->source($user->home_ou);
664 $trans->dest($hold->pickup_lib);
665 $trans->source_send_time("now");
666 $trans->target_copy($copy->id);
667 $trans->copy_status($copy->status);
669 my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
670 my ($stat) = $meth->run( $login_session, $trans, $session );
671 if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
672 else { $copy->status(6); } #status in transit
677 __PACKAGE__->register_method(
678 method => "create_hold_transit",
679 api_name => "open-ils.circ.hold_transit.create",
681 Creates a new transit object
684 sub create_hold_transit {
685 my( $self, $client, $login_session, $transit, $session ) = @_;
687 my( $user, $evt ) = $apputils->checkses($login_session);
689 $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
693 if($session) { $ses = $session; }
694 else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
696 return $ses->request(
697 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
703 sub find_local_hold {
704 my( $class, $session, $copy, $user ) = @_;
705 return $class->find_nearest_permitted_hold($session, $copy, $user);
713 sub fetch_open_hold_by_current_copy {
716 my $hold = $apputils->simplereq(
718 'open-ils.cstore.direct.action.hold_request.search.atomic',
719 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
720 return $hold->[0] if ref($hold);
724 sub fetch_related_holds {
727 return $apputils->simplereq(
729 'open-ils.cstore.direct.action.hold_request.search.atomic',
730 { current_copy => $copyid , cancel_time => undef, fulfillment_time => undef });
734 __PACKAGE__->register_method (
735 method => "hold_pull_list",
736 api_name => "open-ils.circ.hold_pull_list.retrieve",
738 Returns a list of holds that need to be "pulled"
743 __PACKAGE__->register_method (
744 method => "hold_pull_list",
745 api_name => "open-ils.circ.hold_pull_list.id_list.retrieve",
747 Returns a list of hold ID's that need to be "pulled"
754 my( $self, $conn, $authtoken, $limit, $offset ) = @_;
755 my( $reqr, $evt ) = $U->checkses($authtoken);
758 my $org = $reqr->ws_ou || $reqr->home_ou;
759 # the perm locaiton shouldn't really matter here since holds
760 # will exist all over and VIEW_HOLDS should be universal
761 $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
764 if( $self->api_name =~ /id_list/ ) {
765 return $U->storagereq(
766 'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
767 $org, $limit, $offset );
769 return $U->storagereq(
770 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
771 $org, $limit, $offset );
775 __PACKAGE__->register_method (
776 method => 'fetch_hold_notify',
777 api_name => 'open-ils.circ.hold_notification.retrieve_by_hold',
779 Returns a list of hold notification objects based on hold id.
780 @param authtoken The loggin session key
781 @param holdid The id of the hold whose notifications we want to retrieve
782 @return An array of hold notification objects, event on error.
786 sub fetch_hold_notify {
787 my( $self, $conn, $authtoken, $holdid ) = @_;
788 my( $requestor, $evt ) = $U->checkses($authtoken);
791 ($hold, $evt) = $U->fetch_hold($holdid);
793 ($patron, $evt) = $U->fetch_user($hold->usr);
796 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
799 $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
800 return $U->cstorereq(
801 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
805 __PACKAGE__->register_method (
806 method => 'create_hold_notify',
807 api_name => 'open-ils.circ.hold_notification.create',
809 Creates a new hold notification object
810 @param authtoken The login session key
811 @param notification The hold notification object to create
812 @return ID of the new object on success, Event on error
815 sub create_hold_notify {
816 my( $self, $conn, $authtoken, $notification ) = @_;
817 my( $requestor, $evt ) = $U->checkses($authtoken);
820 ($hold, $evt) = $U->fetch_hold($notification->hold);
822 ($patron, $evt) = $U->fetch_user($hold->usr);
825 # XXX perm depth probably doesn't matter here -- should always be consortium level
826 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
829 # Set the proper notifier
830 $notification->notify_staff($requestor->id);
831 my $id = $U->storagereq(
832 'open-ils.storage.direct.action.hold_notification.create', $notification );
833 return $U->DB_UPDATE_FAILED($notification) unless $id;
834 $logger->info("User ".$requestor->id." successfully created new hold notification $id");
839 __PACKAGE__->register_method(
840 method => 'reset_hold',
841 api_name => 'open-ils.circ.hold.reset',
843 Un-captures and un-targets a hold, essentially returning
844 it to the state it was in directly after it was placed,
845 then attempts to re-target the hold
846 @param authtoken The login session key
847 @param holdid The id of the hold
853 my( $self, $conn, $auth, $holdid ) = @_;
855 my ($hold, $evt) = $U->fetch_hold($holdid);
857 ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
859 $evt = $self->_reset_hold($reqr, $hold);
865 my ($self, $reqr, $hold) = @_;
867 my $e = new_editor(xact =>1, requestor => $reqr);
869 $logger->info("reseting hold ".$hold->id);
873 if( $hold->capture_time and $hold->current_copy ) {
875 my $copy = $e->retrieve_asset_copy($hold->current_copy)
878 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
879 $logger->info("setting copy to status 'reshelving' on hold retarget");
880 $copy->status(OILS_COPY_STATUS_RESHELVING);
881 $copy->editor($e->requestor->id);
882 $copy->edit_date('now');
883 $e->update_asset_copy($copy) or return $e->event;
885 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
887 # We don't want the copy to remain "in transit"
888 $copy->status(OILS_COPY_STATUS_RESHELVING);
889 $logger->warn("! reseting hold [$hid] that is in transit");
890 my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
893 my $trans = $e->retrieve_action_transit_copy($transid);
895 $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
896 my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
897 $logger->info("Transit abort completed with result $evt");
898 return $evt unless "$evt" eq 1;
904 $hold->clear_capture_time;
905 $hold->clear_current_copy;
907 $e->update_action_hold_request($hold) or return $e->event;
911 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
917 __PACKAGE__->register_method(
918 method => 'fetch_open_title_holds',
919 api_name => 'open-ils.circ.open_holds.retrieve',
921 Returns a list ids of un-fulfilled holds for a given title id
922 @param authtoken The login session key
923 @param id the id of the item whose holds we want to retrieve
924 @param type The hold type - M, T, V, C
928 sub fetch_open_title_holds {
929 my( $self, $conn, $auth, $id, $type, $org ) = @_;
930 my $e = new_editor( authtoken => $auth );
931 return $e->event unless $e->checkauth;
934 $org ||= $e->requestor->ws_ou;
936 # return $e->search_action_hold_request(
937 # { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
939 # XXX make me return IDs in the future ^--
940 my $holds = $e->search_action_hold_request(
943 cancel_time => undef,
945 fulfillment_time => undef
949 flesh_hold_transits($holds);
954 sub flesh_hold_transits {
956 for my $hold ( @$holds ) {
958 $apputils->simplereq(
960 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
961 { hold => $hold->id },
962 { order_by => { ahtc => 'id desc' }, limit => 1 }
968 sub flesh_hold_notices {
969 my( $holds, $e ) = @_;
972 for my $hold (@$holds) {
973 my $notices = $e->search_action_hold_notification(
975 { hold => $hold->id },
976 { order_by => { anh => { 'notify_time desc' } } },
981 $hold->notify_count(scalar(@$notices));
983 my $n = $e->retrieve_action_hold_notification($$notices[0])
985 $hold->notify_time($n->notify_time);
993 __PACKAGE__->register_method(
994 method => 'fetch_captured_holds',
995 api_name => 'open-ils.circ.captured_holds.on_shelf.retrieve',
997 Returns a list of un-fulfilled holds for a given title id
998 @param authtoken The login session key
999 @param org The org id of the location in question
1003 __PACKAGE__->register_method(
1004 method => 'fetch_captured_holds',
1005 api_name => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
1007 Returns a list ids of un-fulfilled holds for a given title id
1008 @param authtoken The login session key
1009 @param org The org id of the location in question
1013 sub fetch_captured_holds {
1014 my( $self, $conn, $auth, $org ) = @_;
1016 my $e = new_editor(authtoken => $auth);
1017 return $e->event unless $e->checkauth;
1018 return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
1020 $org ||= $e->requestor->ws_ou;
1022 my $holds = $e->search_action_hold_request(
1024 capture_time => { "!=" => undef },
1025 current_copy => { "!=" => undef },
1026 fulfillment_time => undef,
1028 cancel_time => undef,
1033 for my $h (@$holds) {
1034 my $copy = $e->retrieve_asset_copy($h->current_copy)
1035 or return $e->event;
1037 $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
1040 if( ! $self->api_name =~ /id_list/ ) {
1041 flesh_hold_transits(\@res);
1042 flesh_hold_notices(\@res, $e);
1045 if( $self->api_name =~ /id_list/ ) {
1046 return [ map { $_->id } @res ];
1053 __PACKAGE__->register_method(
1054 method => "check_title_hold",
1055 api_name => "open-ils.circ.title_hold.is_possible",
1057 Determines if a hold were to be placed by a given user,
1058 whether or not said hold would have any potential copies
1060 @param authtoken The login session key
1061 @param params A hash of named params including:
1062 patronid - the id of the hold recipient
1063 titleid (brn) - the id of the title to be held
1064 depth - the hold range depth (defaults to 0)
1067 sub check_title_hold {
1068 my( $self, $client, $authtoken, $params ) = @_;
1070 my %params = %$params;
1071 my $titleid = $params{titleid} ||"";
1072 my $volid = $params{volume_id};
1073 my $copyid = $params{copy_id};
1074 my $mrid = $params{mrid} ||"";
1075 my $depth = $params{depth} || 0;
1076 my $pickup_lib = $params{pickup_lib};
1077 my $hold_type = $params{hold_type} || 'T';
1079 my $e = new_editor(authtoken=>$authtoken);
1080 return $e->event unless $e->checkauth;
1081 my $patron = $e->retrieve_actor_user($params{patronid})
1082 or return $e->event;
1084 if( $e->requestor->id ne $patron->id ) {
1085 return $e->event unless
1086 $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
1089 return OpenILS::Event->new('PATRON_BARRED')
1090 if $patron->barred and
1091 ($patron->barred =~ /t/i or $patron->barred == 1);
1093 my $rangelib = $params{range_lib} || $patron->home_ou;
1095 my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1096 or return $e->event;
1098 $logger->info("checking hold possibility with type $hold_type");
1104 if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1106 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1107 $volume = $e->retrieve_asset_call_number($copy->call_number)
1108 or return $e->event;
1109 $title = $e->retrieve_biblio_record_entry($volume->record)
1110 or return $e->event;
1111 return verify_copy_for_hold(
1112 $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1114 } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1116 $volume = $e->retrieve_asset_call_number($volid)
1117 or return $e->event;
1118 $title = $e->retrieve_biblio_record_entry($volume->record)
1119 or return $e->event;
1121 return _check_volume_hold_is_possible(
1122 $volume, $title, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1124 } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1126 return _check_title_hold_is_possible(
1127 $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1129 } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1131 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1132 my @recs = map { $_->source } @$maps;
1133 for my $rec (@recs) {
1134 return 1 if (_check_title_hold_is_possible(
1135 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
1143 sub _check_title_hold_is_possible {
1144 my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1150 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1152 while( $title = $U->storagereq(
1153 'open-ils.storage.biblio.record_entry.ranged_tree',
1154 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1158 ref($title->call_numbers) and
1159 @{$title->call_numbers};
1161 for my $cn (@{$title->call_numbers}) {
1163 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1165 for my $copy (@{$cn->copies}) {
1166 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1167 return 1 if verify_copy_for_hold(
1168 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1169 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1178 sub _check_volume_hold_is_possible {
1179 my( $vol, $title, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1180 my $copies = new_editor->search_asset_copy({call_number => $vol->id});
1181 $logger->info("checking possibility of volume hold for volume ".$vol->id);
1182 for my $copy ( @$copies ) {
1183 return 1 if verify_copy_for_hold(
1184 $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1191 sub verify_copy_for_hold {
1192 my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1193 $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1194 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1195 { patron => $patron,
1196 requestor => $requestor,
1199 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
1200 pickup_lib => $pickup_lib,
1201 request_lib => $request_lib
1209 sub find_nearest_permitted_hold {
1212 my $session = shift;
1215 my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1217 # first see if this copy has already been selected to fulfill a hold
1218 my $hold = $session->request(
1219 "open-ils.storage.direct.action.hold_request.search_where",
1220 { current_copy => $copy->id, cancel_time => undef, capture_time => undef } )->gather(1);
1223 $logger->info("hold found which can be fulfilled by copy ".$copy->id);
1227 # We know this hold is permitted, so just return it
1228 return $hold if $hold;
1230 $logger->debug("searching for potential holds at org ".
1231 $user->ws_ou." and copy ".$copy->id);
1233 my $holds = $session->request(
1234 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1235 $user->ws_ou, $copy->id, 5 )->gather(1);
1237 return (undef, $evt) unless @$holds;
1239 # for each potential hold, we have to run the permit script
1240 # to make sure the hold is actually permitted.
1242 for my $holdid (@$holds) {
1243 next unless $holdid;
1244 $logger->info("Checking if hold $holdid is permitted for user ".$user->id);
1246 my ($hold) = $U->fetch_hold($holdid);
1248 my ($reqr) = $U->fetch_user($hold->requestor);
1250 return ($hold) if OpenILS::Utils::PermitHold::permit_copy_hold(
1252 patron_id => $hold->usr,
1253 requestor => $reqr->id,
1255 pickup_lib => $hold->pickup_lib,
1256 request_lib => $hold->request_lib
1261 return (undef, $evt);
1269 __PACKAGE__->register_method(
1270 method => 'all_rec_holds',
1271 api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1275 my( $self, $conn, $auth, $title_id, $args ) = @_;
1277 my $e = new_editor(authtoken=>$auth);
1278 $e->checkauth or return $e->event;
1279 $e->allowed('VIEW_HOLD') or return $e->event;
1281 $args ||= { fulfillment_time => undef };
1282 $args->{cancel_time} = undef;
1284 my $resp = { volume_holds => [], copy_holds => [] };
1286 $resp->{title_holds} = $e->search_action_hold_request(
1288 hold_type => OILS_HOLD_TYPE_TITLE,
1289 target => $title_id,
1293 my $vols = $e->search_asset_call_number(
1294 { record => $title_id, deleted => 'f' }, {idlist=>1});
1296 return $resp unless @$vols;
1298 $resp->{volume_holds} = $e->search_action_hold_request(
1300 hold_type => OILS_HOLD_TYPE_VOLUME,
1305 my $copies = $e->search_asset_copy(
1306 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1308 return $resp unless @$copies;
1310 $resp->{copy_holds} = $e->search_action_hold_request(
1312 hold_type => OILS_HOLD_TYPE_COPY,
1324 __PACKAGE__->register_method(
1325 method => 'uber_hold',
1326 api_name => 'open-ils.circ.hold.details.retrieve'
1330 my($self, $client, $auth, $hold_id) = @_;
1331 my $e = new_editor(authtoken=>$auth);
1332 $e->checkauth or return $e->event;
1333 $e->allowed('VIEW_HOLD') or return $e->event;
1337 my $hold = $e->retrieve_action_hold_request(
1342 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1345 ) or return $e->event;
1347 my $user = $hold->usr;
1348 $hold->usr($user->id);
1350 my $card = $e->retrieve_actor_card($user->card)
1351 or return $e->event;
1353 my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1355 flesh_hold_notices([$hold], $e);
1356 flesh_hold_transits([$hold]);
1363 status => _hold_status($e, $hold),
1364 patron_first => $user->first_given_name,
1365 patron_last => $user->family_name,
1366 patron_barcode => $card->barcode,
1372 # -----------------------------------------------------
1373 # Returns the MVR object that represents what the
1375 # -----------------------------------------------------
1377 my( $e, $hold ) = @_;
1383 if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1384 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1385 or return $e->event;
1386 $tid = $mr->master_record;
1388 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1389 $tid = $hold->target;
1391 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1392 $volume = $e->retrieve_asset_call_number($hold->target)
1393 or return $e->event;
1394 $tid = $volume->record;
1396 } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1397 $copy = $e->retrieve_asset_copy($hold->target)
1398 or return $e->event;
1399 $volume = $e->retrieve_asset_call_number($copy->call_number)
1400 or return $e->event;
1401 $tid = $volume->record;
1404 if(!$copy and ref $hold->current_copy ) {
1405 $copy = $hold->current_copy;
1406 $hold->current_copy($copy->id);
1409 if(!$volume and $copy) {
1410 $volume = $e->retrieve_asset_call_number($copy->call_number);
1413 my $title = $e->retrieve_biblio_record_entry($tid);
1414 return ( $U->record_to_mvr($title), $volume, $copy );