]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
removed a pile of deprecated code
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Circ / Holds.pm
1 # ---------------------------------------------------------------
2 # Copyright (C) 2005  Georgia Public Library Service 
3 # Bill Erickson <highfalutin@gmail.com>
4
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.
9
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 # ---------------------------------------------------------------
15
16
17 package OpenILS::Application::Circ::Holds;
18 use base qw/OpenSRF::Application/;
19 use strict; use warnings;
20 use OpenILS::Application::AppUtils;
21 use Data::Dumper;
22 use OpenSRF::EX qw(:try);
23 use OpenILS::Perm;
24 use OpenILS::Event;
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;
31
32 my $apputils = "OpenILS::Application::AppUtils";
33 my $U = $apputils;
34
35
36
37 __PACKAGE__->register_method(
38         method  => "create_hold",
39         api_name        => "open-ils.circ.holds.create",
40         notes           => <<NOTE);
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
44 the hold object.
45
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.
51
52 If the recipient does not have permission to place multiple holds
53 on a single title and said operation is attempted, a permission
54 exception is returned
55 NOTE
56
57
58 __PACKAGE__->register_method(
59         method  => "create_hold",
60         api_name        => "open-ils.circ.holds.create.override",
61         signature       => q/
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
65         /
66 );
67
68 sub create_hold {
69         my( $self, $conn, $auth, @holds ) = @_;
70         my $e = new_editor(authtoken=>$auth, xact=>1);
71         return $e->event unless $e->checkauth;
72
73         my $override = 1 if $self->api_name =~ /override/;
74
75         my $holds = (ref($holds[0] eq 'ARRAY')) ? $holds[0] : [@holds];
76
77 #       my @copyholds;
78
79         for my $hold (@$holds) {
80
81                 next unless $hold;
82                 my @events;
83
84                 my $requestor = $e->requestor;
85                 my $recipient = $requestor;
86
87
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;
93                 }
94
95                 # Now make sure the recipient is allowed to receive the specified hold
96                 my $pevt;
97                 my $porg                = $recipient->home_ou;
98                 my $rid         = $e->requestor->id;
99                 my $t                   = $hold->hold_type;
100
101                 # See if a duplicate hold already exists
102                 my $sargs = {
103                         usr                     => $recipient->id, 
104                         hold_type       => $t, 
105                         fulfillment_time => undef, 
106                         target          => $hold->target,
107                         cancel_time     => undef,
108                 };
109
110                 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
111                         
112                 my $existing = $e->search_action_hold_request($sargs); 
113                 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
114
115                 if( $t eq OILS_HOLD_TYPE_METARECORD ) 
116                         { $pevt = $e->event unless $e->checkperm($rid, $porg, 'MR_HOLDS'); }
117
118                 if( $t eq OILS_HOLD_TYPE_TITLE ) 
119                         { $pevt = $e->event unless $e->checkperm($rid, $porg, 'TITLE_HOLDS');  }
120
121                 if( $t eq OILS_HOLD_TYPE_VOLUME ) 
122                         { $pevt = $e->event unless $e->checkperm($rid, $porg, 'VOLUME_HOLDS'); }
123
124                 if( $t eq OILS_HOLD_TYPE_COPY ) 
125                         { $pevt = $e->event unless $e->checkperm($rid, $porg, 'COPY_HOLDS'); }
126
127                 return $pevt if $pevt;
128
129                 if( @events ) {
130                         if( $override ) {
131                                 for my $evt (@events) {
132                                         next unless $evt;
133                                         my $name = $evt->{textcode};
134                                         return $e->event unless $e->allowed("$name.override", $porg);
135                                 }
136                         } else {
137                                 return \@events;
138                         }
139                 }
140
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;
146         }
147
148         $e->commit;
149
150         $conn->respond_complete(1);
151
152         # Go ahead and target the copy-level holds
153         $U->storagereq(
154                 'open-ils.storage.action.hold_request.copy_targeter', 
155                 undef, $_->id ) for @holds;
156
157         return undef;
158 }
159
160 sub __create_hold {
161         my( $self, $client, $login_session, @holds) = @_;
162
163         if(!@holds){return 0;}
164         my( $user, $evt ) = $apputils->checkses($login_session);
165         return $evt if $evt;
166
167         my $holds;
168         if(ref($holds[0]) eq 'ARRAY') {
169                 $holds = $holds[0];
170         } else { $holds = [ @holds ]; }
171
172         $logger->debug("Iterating over holds requests...");
173
174         for my $hold (@$holds) {
175
176                 if(!$hold){next};
177                 my $type = $hold->hold_type;
178
179                 $logger->activity("User " . $user->id . 
180                         " creating new hold of type $type for user " . $hold->usr);
181
182                 my $recipient;
183                 if($user->id ne $hold->usr) {
184                         ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
185                         return $evt if $evt;
186
187                 } else {
188                         $recipient = $user;
189                 }
190
191
192                 my $perm = undef;
193
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; }
198                 }
199
200                 # is this user allowed to have holds of this type?
201                 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
202                 if($perm) { 
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;}
207
208                         } else {
209                                 return $perm; 
210                         }
211                 }
212
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;
216
217                 my $resp = $apputils->simplereq(
218                         'open-ils.storage',
219                         'open-ils.storage.direct.action.hold_request.create', $hold );
220
221                 if(!$resp) { 
222                         return OpenSRF::EX::ERROR ("Error creating hold"); 
223                 }
224         }
225
226         return 1;
227 }
228
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) = @_;
233
234         my $evt;
235         if($type eq "M") {
236                 if($evt = $apputils->check_perms(
237                         $user_id, $org_id, "MR_HOLDS")) {
238                         return $evt;
239                 } 
240
241         } elsif ($type eq "T") {
242                 if($evt = $apputils->check_perms(
243                         $user_id, $org_id, "TITLE_HOLDS")) {
244                         return $evt;
245                 }
246
247         } elsif($type eq "V") {
248                 if($evt = $apputils->check_perms(
249                         $user_id, $org_id, "VOLUME_HOLDS")) {
250                         return $evt;
251                 }
252
253         } elsif($type eq "C") {
254                 if($evt = $apputils->check_perms(
255                         $user_id, $org_id, "COPY_HOLDS")) {
256                         return $evt;
257                 }
258         }
259
260         return undef;
261 }
262
263 # tests if the given user is allowed to place holds on another's behalf
264 sub _check_request_holds_perm {
265         my $user_id = shift;
266         my $org_id = shift;
267         if(my $evt = $apputils->check_perms(
268                 $user_id, $org_id, "REQUEST_HOLDS")) {
269                 return $evt;
270         }
271 }
272
273 sub _check_request_holds_override {
274         my $user_id = shift;
275         my $org_id = shift;
276         if(my $evt = $apputils->check_perms(
277                 $user_id, $org_id, "REQUEST_HOLDS_OVERRIDE")) {
278                 return $evt;
279         }
280 }
281
282 __PACKAGE__->register_method(
283         method  => "retrieve_holds_by_id",
284         api_name        => "open-ils.circ.holds.retrieve_by_id",
285         notes           => <<NOTE);
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.
288 NOTE
289
290
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;
296
297         my $holds = $e->search_action_hold_request(
298                 [
299                         { id =>  $hold_id , fulfillment_time => undef }, 
300                         { order_by => { ahr => "request_time" } }
301                 ]
302         );
303
304         flesh_hold_transits($holds);
305         flesh_hold_notices($holds, $e);
306         return $holds;
307 }
308
309
310 __PACKAGE__->register_method(
311         method  => "retrieve_holds",
312         api_name        => "open-ils.circ.holds.retrieve",
313         notes           => <<NOTE);
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.
317 NOTE
318
319 __PACKAGE__->register_method(
320         method  => "retrieve_holds",
321         api_name        => "open-ils.circ.holds.id_list.retrieve",
322         notes           => <<NOTE);
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.
326 NOTE
327
328 sub retrieve_holds {
329         my($self, $client, $login_session, $user_id) = @_;
330
331         my( $user, $target, $evt ) = $apputils->checkses_requestor(
332                 $login_session, $user_id, 'VIEW_HOLD' );
333         return $evt if $evt;
334
335         my $holds = $apputils->simplereq(
336                 'open-ils.cstore',
337                 "open-ils.cstore.direct.action.hold_request.search.atomic",
338                 { 
339                         usr =>  $user_id , 
340                         fulfillment_time => undef,
341                         cancel_time => undef,
342                 }, 
343                 { order_by => { ahr => "request_time" } }
344         );
345         
346         if( ! $self->api_name =~ /id_list/ ) {
347                 for my $hold ( @$holds ) {
348                         $hold->transit(
349                                 $apputils->simplereq(
350                                         'open-ils.cstore',
351                                         "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
352                                         { hold => $hold->id },
353                                         { order_by => { ahtc => 'id desc' }, limit => 1 }
354                                 )->[0]
355                         );
356                 }
357         }
358
359         if( $self->api_name =~ /id_list/ ) {
360                 return [ map { $_->id } @$holds ];
361         } else {
362                 return $holds;
363         }
364 }
365
366 __PACKAGE__->register_method(
367         method  => "retrieve_holds_by_pickup_lib",
368         api_name        => "open-ils.circ.holds.retrieve_by_pickup_lib",
369         notes           => <<NOTE);
370 Retrieves all the holds, with hold transits attached, for the specified
371 pickup_ou id. 
372 NOTE
373
374 __PACKAGE__->register_method(
375         method  => "retrieve_holds_by_pickup_lib",
376         api_name        => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
377         notes           => <<NOTE);
378 Retrieves all the hold ids for the specified
379 pickup_ou id. 
380 NOTE
381
382 sub retrieve_holds_by_pickup_lib {
383         my($self, $client, $login_session, $ou_id) = @_;
384
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;
389
390         my $holds = $apputils->simplereq(
391                 'open-ils.cstore',
392                 "open-ils.cstore.direct.action.hold_request.search.atomic",
393                 { 
394                         pickup_lib =>  $ou_id , 
395                         fulfillment_time => undef,
396                         cancel_time => undef
397                 }, 
398                 { order_by => { ahr => "request_time" } });
399
400
401         if( ! $self->api_name =~ /id_list/ ) {
402                 flesh_hold_transits($holds);
403         }
404
405         if( $self->api_name =~ /id_list/ ) {
406                 return [ map { $_->id } @$holds ];
407         } else {
408                 return $holds;
409         }
410 }
411
412 __PACKAGE__->register_method(
413         method  => "cancel_hold",
414         api_name        => "open-ils.circ.hold.cancel",
415         notes           => <<"  NOTE");
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
420         NOTE
421
422 sub cancel_hold {
423         my($self, $client, $auth, $holdid) = @_;
424
425         my $e = new_editor(authtoken=>$auth, xact=>1);
426         return $e->event unless $e->checkauth;
427
428         my $hold = $e->retrieve_action_hold_request($holdid)
429                 or return $e->event;
430
431         if( $e->requestor->id ne $hold->usr ) {
432                 return $e->event unless $e->allowed('CANCEL_HOLDS');
433         }
434
435         return 1 if $hold->cancel_time;
436
437         # If the hold is captured, reset the copy status
438         if( $hold->capture_time and $hold->current_copy ) {
439
440                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
441                         or return $e->event;
442
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;
449
450                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
451
452                         my $hid = $hold->id;
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];
455
456                         if( $transid ) {
457                                 my $trans = $e->retrieve_action_transit_copy($transid);
458                                 if( $trans ) {
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;
463                                 }
464                         }
465
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;
473                 }
474         }
475
476         $hold->cancel_time('now');
477         $e->update_action_hold_request($hold)
478                 or return $e->event;
479
480         $self->delete_hold_copy_maps($e, $hold->id);
481
482         $e->commit;
483         return 1;
484 }
485
486 sub delete_hold_copy_maps {
487         my $class = shift;
488         my $editor = shift;
489         my $holdid = shift;
490
491         my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
492         for(@$maps) {
493                 $editor->delete_action_hold_copy_map($_) 
494                         or return $editor->event;
495         }
496         return undef;
497 }
498
499
500 __PACKAGE__->register_method(
501         method  => "update_hold",
502         api_name        => "open-ils.circ.hold.update",
503         notes           => <<"  NOTE");
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.
507         NOTE
508
509 sub update_hold {
510         my($self, $client, $login_session, $hold) = @_;
511
512         my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
513                 $login_session, $hold->usr, 'UPDATE_HOLD' );
514         return $evt if $evt;
515
516         $logger->activity('User ' . $requestor->id . 
517                 ' updating hold ' . $hold->id . ' for user ' . $target->id );
518
519         return $U->storagereq(
520                 "open-ils.storage.direct.action.hold_request.update", $hold );
521 }
522
523
524 __PACKAGE__->register_method(
525         method  => "retrieve_hold_status",
526         api_name        => "open-ils.circ.hold.status.retrieve",
527         notes           => <<"  NOTE");
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'
536         NOTE
537
538 sub retrieve_hold_status {
539         my($self, $client, $auth, $hold_id) = @_;
540
541         my $e = new_editor(authtoken => $auth);
542         return $e->event unless $e->checkauth;
543         my $hold = $e->retrieve_action_hold_request($hold_id)
544                 or return $e->event;
545
546         if( $e->requestor->id != $hold->usr ) {
547                 return $e->event unless $e->allowed('VIEW_HOLD');
548         }
549
550         return _hold_status($e, $hold);
551
552 }
553
554 sub _hold_status {
555         my($e, $hold) = @_;
556         return 1 unless $hold->current_copy;
557         return 2 unless $hold->capture_time;
558
559         my $copy = $hold->current_copy;
560         unless( ref $copy ) {
561                 $copy = $e->retrieve_asset_copy($hold->current_copy)
562                         or return $e->event;
563         }
564
565         return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
566         return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
567
568         return -1;
569 }
570
571
572
573
574 sub find_local_hold {
575         my( $class, $session, $copy, $user ) = @_;
576         return $class->find_nearest_permitted_hold($session, $copy, $user);
577 }
578
579
580 sub fetch_open_hold_by_current_copy {
581         my $class = shift;
582         my $copyid = shift;
583         my $hold = $apputils->simplereq(
584                 'open-ils.cstore', 
585                 'open-ils.cstore.direct.action.hold_request.search.atomic',
586                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
587         return $hold->[0] if ref($hold);
588         return undef;
589 }
590
591 sub fetch_related_holds {
592         my $class = shift;
593         my $copyid = shift;
594         return $apputils->simplereq(
595                 'open-ils.cstore', 
596                 'open-ils.cstore.direct.action.hold_request.search.atomic',
597                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
598 }
599
600
601 __PACKAGE__->register_method (
602         method          => "hold_pull_list",
603         api_name                => "open-ils.circ.hold_pull_list.retrieve",
604         signature       => q/
605                 Returns a list of holds that need to be "pulled"
606                 by a given location
607         /
608 );
609
610 __PACKAGE__->register_method (
611         method          => "hold_pull_list",
612         api_name                => "open-ils.circ.hold_pull_list.id_list.retrieve",
613         signature       => q/
614                 Returns a list of hold ID's that need to be "pulled"
615                 by a given location
616         /
617 );
618
619
620 sub hold_pull_list {
621         my( $self, $conn, $authtoken, $limit, $offset ) = @_;
622         my( $reqr, $evt ) = $U->checkses($authtoken);
623         return $evt if $evt;
624
625         my $org = $reqr->ws_ou || $reqr->home_ou;
626         # the perm locaiton shouldn't really matter here since holds
627         # will exist all over and VIEW_HOLDS should be universal
628         $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
629         return $evt if $evt;
630
631         if( $self->api_name =~ /id_list/ ) {
632                 return $U->storagereq(
633                         'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
634                         $org, $limit, $offset ); 
635         } else {
636                 return $U->storagereq(
637                         'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
638                         $org, $limit, $offset ); 
639         }
640 }
641
642 __PACKAGE__->register_method (
643         method          => 'fetch_hold_notify',
644         api_name                => 'open-ils.circ.hold_notification.retrieve_by_hold',
645         signature       => q/ 
646                 Returns a list of hold notification objects based on hold id.
647                 @param authtoken The loggin session key
648                 @param holdid The id of the hold whose notifications we want to retrieve
649                 @return An array of hold notification objects, event on error.
650         /
651 );
652
653 sub fetch_hold_notify {
654         my( $self, $conn, $authtoken, $holdid ) = @_;
655         my( $requestor, $evt ) = $U->checkses($authtoken);
656         return $evt if $evt;
657         my ($hold, $patron);
658         ($hold, $evt) = $U->fetch_hold($holdid);
659         return $evt if $evt;
660         ($patron, $evt) = $U->fetch_user($hold->usr);
661         return $evt if $evt;
662
663         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
664         return $evt if $evt;
665
666         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
667         return $U->cstorereq(
668                 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
669 }
670
671
672 __PACKAGE__->register_method (
673         method          => 'create_hold_notify',
674         api_name                => 'open-ils.circ.hold_notification.create',
675         signature       => q/
676                 Creates a new hold notification object
677                 @param authtoken The login session key
678                 @param notification The hold notification object to create
679                 @return ID of the new object on success, Event on error
680                 /
681 );
682 sub create_hold_notify {
683         my( $self, $conn, $authtoken, $notification ) = @_;
684         my( $requestor, $evt ) = $U->checkses($authtoken);
685         return $evt if $evt;
686         my ($hold, $patron);
687         ($hold, $evt) = $U->fetch_hold($notification->hold);
688         return $evt if $evt;
689         ($patron, $evt) = $U->fetch_user($hold->usr);
690         return $evt if $evt;
691
692         # XXX perm depth probably doesn't matter here -- should always be consortium level
693         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
694         return $evt if $evt;
695
696         # Set the proper notifier 
697         $notification->notify_staff($requestor->id);
698         my $id = $U->storagereq(
699                 'open-ils.storage.direct.action.hold_notification.create', $notification );
700         return $U->DB_UPDATE_FAILED($notification) unless $id;
701         $logger->info("User ".$requestor->id." successfully created new hold notification $id");
702         return $id;
703 }
704
705
706 __PACKAGE__->register_method(
707         method  => 'reset_hold',
708         api_name        => 'open-ils.circ.hold.reset',
709         signature       => q/
710                 Un-captures and un-targets a hold, essentially returning
711                 it to the state it was in directly after it was placed,
712                 then attempts to re-target the hold
713                 @param authtoken The login session key
714                 @param holdid The id of the hold
715         /
716 );
717
718
719 sub reset_hold {
720         my( $self, $conn, $auth, $holdid ) = @_;
721         my $reqr;
722         my ($hold, $evt) = $U->fetch_hold($holdid);
723         return $evt if $evt;
724         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
725         return $evt if $evt;
726         $evt = $self->_reset_hold($reqr, $hold);
727         return $evt if $evt;
728         return 1;
729 }
730
731 sub _reset_hold {
732         my ($self, $reqr, $hold) = @_;
733
734         my $e = new_editor(xact =>1, requestor => $reqr);
735
736         $logger->info("reseting hold ".$hold->id);
737
738         my $hid = $hold->id;
739
740         if( $hold->capture_time and $hold->current_copy ) {
741
742                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
743                         or return $e->event;
744
745                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
746                         $logger->info("setting copy to status 'reshelving' on hold retarget");
747                         $copy->status(OILS_COPY_STATUS_RESHELVING);
748                         $copy->editor($e->requestor->id);
749                         $copy->edit_date('now');
750                         $e->update_asset_copy($copy) or return $e->event;
751
752                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
753
754                         # We don't want the copy to remain "in transit"
755                         $copy->status(OILS_COPY_STATUS_RESHELVING);
756                         $logger->warn("! reseting hold [$hid] that is in transit");
757                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
758
759                         if( $transid ) {
760                                 my $trans = $e->retrieve_action_transit_copy($transid);
761                                 if( $trans ) {
762                                         $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
763                                         my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
764                                         $logger->info("Transit abort completed with result $evt");
765                                         return $evt unless "$evt" eq 1;
766                                 }
767                         }
768                 }
769         }
770
771         $hold->clear_capture_time;
772         $hold->clear_current_copy;
773
774         $e->update_action_hold_request($hold) or return $e->event;
775         $e->commit;
776
777         $U->storagereq(
778                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
779
780         return undef;
781 }
782
783
784 __PACKAGE__->register_method(
785         method => 'fetch_open_title_holds',
786         api_name        => 'open-ils.circ.open_holds.retrieve',
787         signature       => q/
788                 Returns a list ids of un-fulfilled holds for a given title id
789                 @param authtoken The login session key
790                 @param id the id of the item whose holds we want to retrieve
791                 @param type The hold type - M, T, V, C
792         /
793 );
794
795 sub fetch_open_title_holds {
796         my( $self, $conn, $auth, $id, $type, $org ) = @_;
797         my $e = new_editor( authtoken => $auth );
798         return $e->event unless $e->checkauth;
799
800         $type ||= "T";
801         $org ||= $e->requestor->ws_ou;
802
803 #       return $e->search_action_hold_request(
804 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
805
806         # XXX make me return IDs in the future ^--
807         my $holds = $e->search_action_hold_request(
808                 { 
809                         target                          => $id, 
810                         cancel_time                     => undef, 
811                         hold_type                       => $type, 
812                         fulfillment_time        => undef 
813                 }
814         );
815
816         flesh_hold_transits($holds);
817         return $holds;
818 }
819
820
821 sub flesh_hold_transits {
822         my $holds = shift;
823         for my $hold ( @$holds ) {
824                 $hold->transit(
825                         $apputils->simplereq(
826                                 'open-ils.cstore',
827                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
828                                 { hold => $hold->id },
829                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
830                         )->[0]
831                 );
832         }
833 }
834
835 sub flesh_hold_notices {
836         my( $holds, $e ) = @_;
837         $e ||= new_editor();
838
839         for my $hold (@$holds) {
840                 my $notices = $e->search_action_hold_notification(
841                         [
842                                 { hold => $hold->id },
843                                 { order_by => { anh => { 'notify_time desc' } } },
844                         ],
845                         {idlist=>1}
846                 );
847
848                 $hold->notify_count(scalar(@$notices));
849                 if( @$notices ) {
850                         my $n = $e->retrieve_action_hold_notification($$notices[0])
851                                 or return $e->event;
852                         $hold->notify_time($n->notify_time);
853                 }
854         }
855 }
856
857
858
859
860 __PACKAGE__->register_method(
861         method => 'fetch_captured_holds',
862         api_name        => 'open-ils.circ.captured_holds.on_shelf.retrieve',
863         signature       => q/
864                 Returns a list of un-fulfilled holds for a given title id
865                 @param authtoken The login session key
866                 @param org The org id of the location in question
867         /
868 );
869
870 __PACKAGE__->register_method(
871         method => 'fetch_captured_holds',
872         api_name        => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
873         signature       => q/
874                 Returns a list ids of un-fulfilled holds for a given title id
875                 @param authtoken The login session key
876                 @param org The org id of the location in question
877         /
878 );
879
880 sub fetch_captured_holds {
881         my( $self, $conn, $auth, $org ) = @_;
882
883         my $e = new_editor(authtoken => $auth);
884         return $e->event unless $e->checkauth;
885         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
886
887         $org ||= $e->requestor->ws_ou;
888
889         my $holds = $e->search_action_hold_request(
890                 { 
891                         capture_time            => { "!=" => undef },
892                         current_copy            => { "!=" => undef },
893                         fulfillment_time        => undef,
894                         pickup_lib                      => $org,
895                         cancel_time                     => undef,
896                 }
897         );
898
899         my @res;
900         for my $h (@$holds) {
901                 my $copy = $e->retrieve_asset_copy($h->current_copy)
902                         or return $e->event;
903                 push( @res, $h ) if 
904                         $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
905         }
906
907         if( ! $self->api_name =~ /id_list/ ) {
908                 flesh_hold_transits(\@res);
909                 flesh_hold_notices(\@res, $e);
910         }
911
912         if( $self->api_name =~ /id_list/ ) {
913                 return [ map { $_->id } @res ];
914         } else {
915                 return \@res;
916         }
917 }
918
919
920 __PACKAGE__->register_method(
921         method  => "check_title_hold",
922         api_name        => "open-ils.circ.title_hold.is_possible",
923         notes           => q/
924                 Determines if a hold were to be placed by a given user,
925                 whether or not said hold would have any potential copies
926                 to fulfill it.
927                 @param authtoken The login session key
928                 @param params A hash of named params including:
929                         patronid  - the id of the hold recipient
930                         titleid (brn) - the id of the title to be held
931                         depth   - the hold range depth (defaults to 0)
932         /);
933
934 sub check_title_hold {
935         my( $self, $client, $authtoken, $params ) = @_;
936
937         my %params              = %$params;
938         my $titleid             = $params{titleid} ||"";
939         my $volid               = $params{volume_id};
940         my $copyid              = $params{copy_id};
941         my $mrid                        = $params{mrid} ||"";
942         my $depth               = $params{depth} || 0;
943         my $pickup_lib  = $params{pickup_lib};
944         my $hold_type   = $params{hold_type} || 'T';
945
946         my $e = new_editor(authtoken=>$authtoken);
947         return $e->event unless $e->checkauth;
948         my $patron = $e->retrieve_actor_user($params{patronid})
949                 or return $e->event;
950
951         if( $e->requestor->id ne $patron->id ) {
952                 return $e->event unless 
953                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
954         }
955
956         return OpenILS::Event->new('PATRON_BARRED') 
957                 if $patron->barred and 
958                         ($patron->barred =~ /t/i or $patron->barred == 1);
959
960         my $rangelib    = $params{range_lib} || $patron->home_ou;
961
962         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
963                 or return $e->event;
964
965         $logger->info("checking hold possibility with type $hold_type");
966
967         my $copy;
968         my $volume;
969         my $title;
970
971         if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
972
973                 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
974                 $volume = $e->retrieve_asset_call_number($copy->call_number)
975                         or return $e->event;
976                 $title = $e->retrieve_biblio_record_entry($volume->record)
977                         or return $e->event;
978                 return verify_copy_for_hold( 
979                         $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
980
981         } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
982
983                 $volume = $e->retrieve_asset_call_number($volid)
984                         or return $e->event;
985                 $title = $e->retrieve_biblio_record_entry($volume->record)
986                         or return $e->event;
987
988                 return _check_volume_hold_is_possible(
989                         $volume, $title, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
990
991         } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
992
993                 return _check_title_hold_is_possible(
994                         $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
995
996         } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
997
998                 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
999                 my @recs = map { $_->source } @$maps;
1000                 for my $rec (@recs) {
1001                         return 1 if (_check_title_hold_is_possible(
1002                                 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
1003                 }
1004                 return 0;       
1005         }
1006 }
1007
1008
1009
1010 sub _check_title_hold_is_possible {
1011         my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1012
1013         my $limit       = 10;
1014         my $offset      = 0;
1015         my $title;
1016
1017         $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1018
1019         while( $title = $U->storagereq(
1020                                 'open-ils.storage.biblio.record_entry.ranged_tree', 
1021                                 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1022
1023                 last unless 
1024                         ref($title) and 
1025                         ref($title->call_numbers) and 
1026                         @{$title->call_numbers};
1027
1028                 for my $cn (@{$title->call_numbers}) {
1029         
1030                         $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1031         
1032                         for my $copy (@{$cn->copies}) {
1033                                 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1034                                 return 1 if verify_copy_for_hold( 
1035                                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1036                                 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1037                         }
1038                 }
1039
1040                 $offset += $limit;
1041         }
1042         return 0;
1043 }
1044
1045 sub _check_volume_hold_is_possible {
1046         my( $vol, $title, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1047         my $copies = new_editor->search_asset_copy({call_number => $vol->id});
1048         $logger->info("checking possibility of volume hold for volume ".$vol->id);
1049         for my $copy ( @$copies ) {
1050                 return 1 if verify_copy_for_hold( 
1051                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1052         }
1053         return 0;
1054 }
1055
1056
1057
1058 sub verify_copy_for_hold {
1059         my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1060         $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1061         return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1062                 {       patron                          => $patron, 
1063                         requestor                       => $requestor, 
1064                         copy                                    => $copy,
1065                         title                                   => $title, 
1066                         title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1067                         pickup_lib                      => $pickup_lib,
1068                         request_lib                     => $request_lib 
1069                 } 
1070         );
1071         return 0;
1072 }
1073
1074
1075
1076 sub find_nearest_permitted_hold {
1077
1078         my $class       = shift;
1079         my $session = shift;
1080         my $copy                = shift;
1081         my $user                = shift;
1082         my $evt         = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1083
1084         # first see if this copy has already been selected to fulfill a hold
1085         my $hold  = $session->request(
1086                 "open-ils.storage.direct.action.hold_request.search_where",
1087                 { current_copy => $copy->id, cancel_time => undef, capture_time => undef } )->gather(1);
1088
1089         if( $hold ) {
1090                 $logger->info("hold found which can be fulfilled by copy ".$copy->id);
1091                 return $hold;
1092         }
1093
1094         # We know this hold is permitted, so just return it
1095         return $hold if $hold;
1096
1097         $logger->debug("searching for potential holds at org ". 
1098                 $user->ws_ou." and copy ".$copy->id);
1099
1100         my $holds = $session->request(
1101                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1102                 $user->ws_ou, $copy->id, 5 )->gather(1);
1103
1104         return (undef, $evt) unless @$holds;
1105
1106         # for each potential hold, we have to run the permit script
1107         # to make sure the hold is actually permitted.
1108
1109         for my $holdid (@$holds) {
1110                 next unless $holdid;
1111                 $logger->info("Checking if hold $holdid is permitted for user ".$user->id);
1112
1113                 my ($hold) = $U->fetch_hold($holdid);
1114                 next unless $hold;
1115                 my ($reqr) = $U->fetch_user($hold->requestor);
1116
1117                 my ($rlib) = $U->fetch_org_unit($hold->request_lib);
1118
1119                 return ($hold) if OpenILS::Utils::PermitHold::permit_copy_hold(
1120                         {
1121                                 patron_id                       => $hold->usr,
1122                                 requestor                       => $reqr->id,
1123                                 copy                                    => $copy,
1124                                 pickup_lib                      => $hold->pickup_lib,
1125                                 request_lib                     => $rlib,
1126                         } 
1127                 );
1128         }
1129
1130         return (undef, $evt);
1131 }
1132
1133
1134
1135
1136
1137
1138 __PACKAGE__->register_method(
1139         method => 'all_rec_holds',
1140         api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1141 );
1142
1143 sub all_rec_holds {
1144         my( $self, $conn, $auth, $title_id, $args ) = @_;
1145
1146         my $e = new_editor(authtoken=>$auth);
1147         $e->checkauth or return $e->event;
1148         $e->allowed('VIEW_HOLD') or return $e->event;
1149
1150         $args ||= { fulfillment_time => undef };
1151         $args->{cancel_time} = undef;
1152
1153         my $resp = { volume_holds => [], copy_holds => [] };
1154
1155         $resp->{title_holds} = $e->search_action_hold_request(
1156                 { 
1157                         hold_type => OILS_HOLD_TYPE_TITLE, 
1158                         target => $title_id, 
1159                         %$args 
1160                 }, {idlist=>1} );
1161
1162         my $vols = $e->search_asset_call_number(
1163                 { record => $title_id, deleted => 'f' }, {idlist=>1});
1164
1165         return $resp unless @$vols;
1166
1167         $resp->{volume_holds} = $e->search_action_hold_request(
1168                 { 
1169                         hold_type => OILS_HOLD_TYPE_VOLUME, 
1170                         target => $vols,
1171                         %$args }, 
1172                 {idlist=>1} );
1173
1174         my $copies = $e->search_asset_copy(
1175                 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1176
1177         return $resp unless @$copies;
1178
1179         $resp->{copy_holds} = $e->search_action_hold_request(
1180                 { 
1181                         hold_type => OILS_HOLD_TYPE_COPY,
1182                         target => $copies,
1183                         %$args }, 
1184                 {idlist=>1} );
1185
1186         return $resp;
1187 }
1188
1189
1190
1191
1192
1193 __PACKAGE__->register_method(
1194         method => 'uber_hold',
1195         api_name => 'open-ils.circ.hold.details.retrieve'
1196 );
1197
1198 sub uber_hold {
1199         my($self, $client, $auth, $hold_id) = @_;
1200         my $e = new_editor(authtoken=>$auth);
1201         $e->checkauth or return $e->event;
1202         $e->allowed('VIEW_HOLD') or return $e->event;
1203
1204         my $resp = {};
1205
1206         my $hold = $e->retrieve_action_hold_request(
1207                 [
1208                         $hold_id,
1209                         {
1210                                 flesh => 1,
1211                                 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1212                         }
1213                 ]
1214         ) or return $e->event;
1215
1216         my $user = $hold->usr;
1217         $hold->usr($user->id);
1218
1219         my $card = $e->retrieve_actor_card($user->card)
1220                 or return $e->event;
1221
1222         my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1223
1224         flesh_hold_notices([$hold], $e);
1225         flesh_hold_transits([$hold]);
1226
1227         return {
1228                 hold            => $hold,
1229                 copy            => $copy,
1230                 volume  => $volume,
1231                 mvr             => $mvr,
1232                 status  => _hold_status($e, $hold),
1233                 patron_first => $user->first_given_name,
1234                 patron_last  => $user->family_name,
1235                 patron_barcode => $card->barcode,
1236         };
1237 }
1238
1239
1240
1241 # -----------------------------------------------------
1242 # Returns the MVR object that represents what the
1243 # hold is all about
1244 # -----------------------------------------------------
1245 sub find_hold_mvr {
1246         my( $e, $hold ) = @_;
1247
1248         my $tid;
1249         my $copy;
1250         my $volume;
1251
1252         if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1253                 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1254                         or return $e->event;
1255                 $tid = $mr->master_record;
1256
1257         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1258                 $tid = $hold->target;
1259
1260         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1261                 $volume = $e->retrieve_asset_call_number($hold->target)
1262                         or return $e->event;
1263                 $tid = $volume->record;
1264
1265         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1266                 $copy = $e->retrieve_asset_copy($hold->target)
1267                         or return $e->event;
1268                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1269                         or return $e->event;
1270                 $tid = $volume->record;
1271         }
1272
1273         if(!$copy and ref $hold->current_copy ) {
1274                 $copy = $hold->current_copy;
1275                 $hold->current_copy($copy->id);
1276         }
1277
1278         if(!$volume and $copy) {
1279                 $volume = $e->retrieve_asset_call_number($copy->call_number);
1280         }
1281
1282         my $title = $e->retrieve_biblio_record_entry($tid);
1283         return ( $U->record_to_mvr($title), $volume, $copy );
1284 }
1285
1286
1287
1288
1289 1;