]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
simplified hold status logic to rely on existing data
[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
31 my $apputils = "OpenILS::Application::AppUtils";
32 my $U = $apputils;
33
34
35
36 __PACKAGE__->register_method(
37         method  => "create_hold",
38         api_name        => "open-ils.circ.holds.create",
39         notes           => <<NOTE);
40 Create a new hold for an item.  From a permissions perspective, 
41 the login session is used as the 'requestor' of the hold.  
42 The hold recipient is determined by the 'usr' setting within
43 the hold object.
44
45 First we verify the requestion has holds request permissions.
46 Then we verify that the recipient is allowed to make the given hold.
47 If not, we see if the requestor has "override" capabilities.  If not,
48 a permission exception is returned.  If permissions allow, we cycle
49 through the set of holds objects and create.
50
51 If the recipient does not have permission to place multiple holds
52 on a single title and said operation is attempted, a permission
53 exception is returned
54 NOTE
55
56
57 __PACKAGE__->register_method(
58         method  => "create_hold",
59         api_name        => "open-ils.circ.holds.create.override",
60         signature       => q/
61                 If the recipient is not allowed to receive the requested hold,
62                 call this method to attempt the override
63                 @see open-ils.circ.holds.create
64         /
65 );
66
67 sub create_hold {
68         my( $self, $conn, $auth, @holds ) = @_;
69         my $e = new_editor(authtoken=>$auth, xact=>1);
70         return $e->event unless $e->checkauth;
71
72         my $override = 1 if $self->api_name =~ /override/;
73
74         my $holds = (ref($holds[0] eq 'ARRAY')) ? $holds[0] : [@holds];
75
76         for my $hold (@$holds) {
77
78                 next unless $hold;
79                 my @events;
80
81                 my $requestor = $e->requestor;
82                 my $recipient = $requestor;
83
84
85                 if( $requestor->id ne $hold->usr ) {
86                         # Make sure the requestor is allowed to place holds for 
87                         # the recipient if they are not the same people
88                         $recipient = $e->retrieve_actor_user($hold->usr) or return $e->event;
89                         $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->event;
90                 }
91
92
93                 # Now make sure the recipient is allowed to receive the specified hold
94                 my $pevt;
95                 my $porg                = $recipient->home_ou;
96                 my $rid         = $e->requestor->id;
97                 my $t                   = $hold->hold_type;
98
99                 # See if a duplicate hold already exists
100                 my $sargs = {
101                         usr                     => $recipient->id, 
102                         hold_type       => $t, 
103                         fulfillment_time => undef, 
104                         target          => $hold->target,
105                         cancel_time     => undef,
106                 };
107
108                 $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
109                         
110                 my $existing = $e->search_action_hold_request($sargs); 
111                 push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
112
113                 if( $t eq 'M' ) { $pevt = $e->event unless $e->checkperm($rid, $porg, 'MR_HOLDS'); }
114                 if( $t eq 'T' ) { $pevt = $e->event unless $e->checkperm($rid, $porg, 'TITLE_HOLDS');  }
115                 if( $t eq 'V' ) { $pevt = $e->event unless $e->checkperm($rid, $porg, 'VOLUME_HOLDS'); }
116                 if( $t eq 'C' ) { $pevt = $e->event unless $e->checkperm($rid, $porg, 'COPY_HOLDS'); }
117
118                 return $pevt if $pevt;
119
120                 if( @events ) {
121                         if( $override ) {
122                                 for my $evt (@events) {
123                                         next unless $evt;
124                                         my $name = $evt->{textcode};
125                                         return $e->event unless $e->allowed("$name.override", $porg);
126                                 }
127                         } else {
128                                 return \@events;
129                         }
130                 }
131
132
133 #               if( $eevt ) {
134 #                       if( $override ) {
135 #                               return $e->event unless $e->allowed('CREATE_DUPLICATE_HOLDS', $porg);
136 #                       } else {
137 #                               return $eevt;
138 #                       }
139 #               }
140
141
142                 $hold->requestor($e->requestor->id); 
143                 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
144                 $e->create_action_hold_request($hold) or return $e->event;
145         }
146
147         $e->commit;
148         return 1;
149 }
150
151 sub __create_hold {
152         my( $self, $client, $login_session, @holds) = @_;
153
154         if(!@holds){return 0;}
155         my( $user, $evt ) = $apputils->checkses($login_session);
156         return $evt if $evt;
157
158         my $holds;
159         if(ref($holds[0]) eq 'ARRAY') {
160                 $holds = $holds[0];
161         } else { $holds = [ @holds ]; }
162
163         $logger->debug("Iterating over holds requests...");
164
165         for my $hold (@$holds) {
166
167                 if(!$hold){next};
168                 my $type = $hold->hold_type;
169
170                 $logger->activity("User " . $user->id . 
171                         " creating new hold of type $type for user " . $hold->usr);
172
173                 my $recipient;
174                 if($user->id ne $hold->usr) {
175                         ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
176                         return $evt if $evt;
177
178                 } else {
179                         $recipient = $user;
180                 }
181
182
183                 my $perm = undef;
184
185                 # am I allowed to place holds for this user?
186                 if($hold->requestor ne $hold->usr) {
187                         $perm = _check_request_holds_perm($user->id, $user->home_ou);
188                         if($perm) { return $perm; }
189                 }
190
191                 # is this user allowed to have holds of this type?
192                 $perm = _check_holds_perm($type, $hold->requestor, $recipient->home_ou);
193                 if($perm) { 
194                         #if there is a requestor, see if the requestor has override privelages
195                         if($hold->requestor ne $hold->usr) {
196                                 $perm = _check_request_holds_override($user->id, $user->home_ou);
197                                 if($perm) {return $perm;}
198
199                         } else {
200                                 return $perm; 
201                         }
202                 }
203
204                 #enforce the fact that the login is the one requesting the hold
205                 $hold->requestor($user->id); 
206                 $hold->selection_ou($recipient->home_ou) unless $hold->selection_ou;
207
208                 my $resp = $apputils->simplereq(
209                         'open-ils.storage',
210                         'open-ils.storage.direct.action.hold_request.create', $hold );
211
212                 if(!$resp) { 
213                         return OpenSRF::EX::ERROR ("Error creating hold"); 
214                 }
215         }
216
217         return 1;
218 }
219
220 # makes sure that a user has permission to place the type of requested hold
221 # returns the Perm exception if not allowed, returns undef if all is well
222 sub _check_holds_perm {
223         my($type, $user_id, $org_id) = @_;
224
225         my $evt;
226         if($type eq "M") {
227                 if($evt = $apputils->check_perms(
228                         $user_id, $org_id, "MR_HOLDS")) {
229                         return $evt;
230                 } 
231
232         } elsif ($type eq "T") {
233                 if($evt = $apputils->check_perms(
234                         $user_id, $org_id, "TITLE_HOLDS")) {
235                         return $evt;
236                 }
237
238         } elsif($type eq "V") {
239                 if($evt = $apputils->check_perms(
240                         $user_id, $org_id, "VOLUME_HOLDS")) {
241                         return $evt;
242                 }
243
244         } elsif($type eq "C") {
245                 if($evt = $apputils->check_perms(
246                         $user_id, $org_id, "COPY_HOLDS")) {
247                         return $evt;
248                 }
249         }
250
251         return undef;
252 }
253
254 # tests if the given user is allowed to place holds on another's behalf
255 sub _check_request_holds_perm {
256         my $user_id = shift;
257         my $org_id = shift;
258         if(my $evt = $apputils->check_perms(
259                 $user_id, $org_id, "REQUEST_HOLDS")) {
260                 return $evt;
261         }
262 }
263
264 sub _check_request_holds_override {
265         my $user_id = shift;
266         my $org_id = shift;
267         if(my $evt = $apputils->check_perms(
268                 $user_id, $org_id, "REQUEST_HOLDS_OVERRIDE")) {
269                 return $evt;
270         }
271 }
272
273 __PACKAGE__->register_method(
274         method  => "retrieve_holds_by_id",
275         api_name        => "open-ils.circ.holds.retrieve_by_id",
276         notes           => <<NOTE);
277 Retrieve the hold, with hold transits attached, for the specified id The login session is the requestor and if the requestor is
278 different from the user, then the requestor must have VIEW_HOLD permissions.
279 NOTE
280
281
282 sub retrieve_holds_by_id {
283         my($self, $client, $login_session, $hold_id) = @_;
284
285         #FIXME
286         #my( $user, $target, $evt ) = $apputils->checkses_requestor(
287         #       $login_session, $user_id, 'VIEW_HOLD' );
288         #return $evt if $evt;
289
290         my $holds = $apputils->simplereq(
291                 'open-ils.cstore',
292                 "open-ils.cstore.direct.action.hold_request.search.atomic",
293                 { id =>  $hold_id , fulfillment_time => undef }, 
294                 { order_by => { ahr => "request_time" } }
295         );
296
297         flesh_hold_transits($holds);
298         return $holds;
299 }
300
301
302 __PACKAGE__->register_method(
303         method  => "retrieve_holds",
304         api_name        => "open-ils.circ.holds.retrieve",
305         notes           => <<NOTE);
306 Retrieves all the holds, with hold transits attached, for the specified
307 user id.  The login session is the requestor and if the requestor is
308 different from the user, then the requestor must have VIEW_HOLD permissions.
309 NOTE
310
311
312 sub retrieve_holds {
313         my($self, $client, $login_session, $user_id) = @_;
314
315         my( $user, $target, $evt ) = $apputils->checkses_requestor(
316                 $login_session, $user_id, 'VIEW_HOLD' );
317         return $evt if $evt;
318
319         my $holds = $apputils->simplereq(
320                 'open-ils.cstore',
321                 "open-ils.cstore.direct.action.hold_request.search.atomic",
322                 { 
323                         usr =>  $user_id , 
324                         fulfillment_time => undef,
325                         cancel_time => undef,
326                 }, 
327                 { order_by => { ahr => "request_time" } }
328         );
329         
330         for my $hold ( @$holds ) {
331                 $hold->transit(
332                         $apputils->simplereq(
333                                 'open-ils.cstore',
334                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
335                                 { hold => $hold->id },
336                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
337                         )->[0]
338                 );
339         }
340
341         return $holds;
342 }
343
344 __PACKAGE__->register_method(
345         method  => "retrieve_holds_by_pickup_lib",
346         api_name        => "open-ils.circ.holds.retrieve_by_pickup_lib",
347         notes           => <<NOTE);
348 Retrieves all the holds, with hold transits attached, for the specified
349 pickup_ou id. 
350 NOTE
351
352
353 sub retrieve_holds_by_pickup_lib {
354         my($self, $client, $login_session, $ou_id) = @_;
355
356         #FIXME -- put an appropriate permission check here
357         #my( $user, $target, $evt ) = $apputils->checkses_requestor(
358         #       $login_session, $user_id, 'VIEW_HOLD' );
359         #return $evt if $evt;
360
361         my $holds = $apputils->simplereq(
362                 'open-ils.cstore',
363                 "open-ils.cstore.direct.action.hold_request.search.atomic",
364                 { 
365                         pickup_lib =>  $ou_id , 
366                         fulfillment_time => undef,
367                         cancel_time => undef
368                 }, 
369                 { order_by => { ahr => "request_time" } });
370
371
372         flesh_hold_transits($holds);
373         return $holds;
374 }
375
376
377 __PACKAGE__->register_method(
378         method  => "cancel_hold",
379         api_name        => "open-ils.circ.hold.cancel",
380         notes           => <<"  NOTE");
381         Cancels the specified hold.  The login session
382         is the requestor and if the requestor is different from the usr field
383         on the hold, the requestor must have CANCEL_HOLDS permissions.
384         the hold may be either the hold object or the hold id
385         NOTE
386
387 sub cancel_hold {
388         my($self, $client, $auth, $holdid) = @_;
389
390         my $e = new_editor(authtoken=>$auth, xact=>1);
391         return $e->event unless $e->checkauth;
392
393         my $hold = $e->retrieve_action_hold_request($holdid)
394                 or return $e->event;
395
396         if( $e->requestor->id ne $hold->usr ) {
397                 return $e->event unless $e->allowed('CANCEL_HOLDS');
398         }
399
400         return 1 if $hold->cancel_time;
401
402         # If the hold is captured, reset the copy status
403         if( $hold->capture_time and $hold->current_copy ) {
404
405                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
406                         or return $e->event;
407
408                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
409                         $logger->info("setting copy to status 'reshelving' on hold cancel");
410                         $copy->status(OILS_COPY_STATUS_RESHELVING);
411                         $copy->editor($e->requestor->id);
412                         $copy->edit_date('now');
413                         $e->update_asset_copy($copy) or return $e->event;
414                 }
415         }
416
417         $hold->cancel_time('now');
418         $e->update_action_hold_request($hold)
419                 or return $e->event;
420
421         $self->delete_hold_copy_maps($e, $hold->id);
422
423         $e->commit;
424         return 1;
425 }
426
427 sub delete_hold_copy_maps {
428         my $class = shift;
429         my $editor = shift;
430         my $holdid = shift;
431
432         my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
433         for(@$maps) {
434                 $editor->delete_action_hold_copy_map($_) 
435                         or return $editor->event;
436         }
437         return undef;
438 }
439
440
441 __PACKAGE__->register_method(
442         method  => "update_hold",
443         api_name        => "open-ils.circ.hold.update",
444         notes           => <<"  NOTE");
445         Updates the specified hold.  The login session
446         is the requestor and if the requestor is different from the usr field
447         on the hold, the requestor must have UPDATE_HOLDS permissions.
448         NOTE
449
450 sub update_hold {
451         my($self, $client, $login_session, $hold) = @_;
452
453         my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
454                 $login_session, $hold->usr, 'UPDATE_HOLD' );
455         return $evt if $evt;
456
457         $logger->activity('User ' . $requestor->id . 
458                 ' updating hold ' . $hold->id . ' for user ' . $target->id );
459
460         return $U->storagereq(
461                 "open-ils.storage.direct.action.hold_request.update", $hold );
462 }
463
464
465 __PACKAGE__->register_method(
466         method  => "retrieve_hold_status",
467         api_name        => "open-ils.circ.hold.status.retrieve",
468         notes           => <<"  NOTE");
469         Calculates the current status of the hold.
470         the requestor must have VIEW_HOLD permissions if the hold is for a user
471         other than the requestor.
472         Returns -1  on error (for now)
473         Returns 1 for 'waiting for copy to become available'
474         Returns 2 for 'waiting for copy capture'
475         Returns 3 for 'in transit'
476         Returns 4 for 'arrived'
477         NOTE
478
479 sub retrieve_hold_status {
480         my($self, $client, $auth, $hold_id) = @_;
481
482         my $e = new_editor(authtoken => $auth);
483         return $e->event unless $e->checkauth;
484         my $hold = $e->retrieve_action_hold_request($hold_id)
485                 or return $e->event;
486         return $e->event unless $e->allowed('VIEW_HOLD');
487
488         return 1 unless $hold->current_copy;
489         return 2 unless $hold->capture_time;
490
491         my $copy = $e->retrieve_asset_copy($hold->current_copy)
492                 or return $e->event;
493
494         return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
495         return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
496
497         return -1;
498 }
499
500
501
502
503
504 =head DEPRECATED
505 __PACKAGE__->register_method(
506         method  => "capture_copy",
507         api_name        => "open-ils.circ.hold.capture_copy.barcode",
508         notes           => <<"  NOTE");
509         Captures a copy to fulfil a hold
510         Params is login session and copy barcode
511         Optional param is 'flesh'.  If set, we also return the
512         relevant copy and title
513         login mus have COPY_CHECKIN permissions (since this is essentially
514         copy checkin)
515         NOTE
516
517 # XXX deprecate me XXX
518
519 sub capture_copy {
520         my( $self, $client, $login_session, $params ) = @_;
521         my %params = %$params;
522         my $barcode = $params{barcode};
523
524
525         my( $user, $target, $copy, $hold, $evt );
526
527         ( $user, $evt ) = $apputils->checkses($login_session);
528         return $evt if $evt;
529
530         # am I allowed to checkin a copy?
531         $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
532         return $evt if $evt;
533
534         $logger->info("Capturing copy with barcode $barcode");
535
536         my $session = $apputils->start_db_session();
537
538         ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
539         return $evt if $evt;
540
541         $logger->debug("Capturing copy " . $copy->id);
542
543         #( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
544         ( $hold, $evt ) = $self->find_nearest_permitted_hold($session, $copy, $user);
545         return $evt if $evt;
546
547         warn "Found hold " . $hold->id . "\n";
548         $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
549
550         $hold->current_copy($copy->id);
551         $hold->capture_time("now"); 
552
553         #update the hold
554         my $stat = $session->request(
555                         "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
556         if(!$stat) { throw OpenSRF::EX::ERROR 
557                 ("Error updating hold request " . $copy->id); }
558
559         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF); #status on holds shelf
560
561         # if the staff member capturing this item is not at the pickup lib
562         if( $user->home_ou ne $hold->pickup_lib ) {
563                 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
564         }
565
566         $copy->editor($user->id);
567         $copy->edit_date("now");
568         $stat = $session->request(
569                 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
570         if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
571
572         my $payload = { hold => $hold };
573         $payload->{copy} = $copy if $params{flesh_copy};
574
575         if($params{flesh_record}) {
576                 my $record;
577                 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
578                 return $evt if $evt;
579                 $record = $apputils->record_to_mvr($record);
580                 $payload->{record} = $record;
581         }
582
583         $apputils->commit_db_session($session);
584
585         return OpenILS::Event->new('ROUTE_ITEM', 
586                 route_to => $hold->pickup_lib, payload => $payload );
587 }
588
589 sub _build_hold_transit {
590         my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
591         my $trans = Fieldmapper::action::hold_transit_copy->new;
592
593         $trans->hold($hold->id);
594         $trans->source($user->home_ou);
595         $trans->dest($hold->pickup_lib);
596         $trans->source_send_time("now");
597         $trans->target_copy($copy->id);
598         $trans->copy_status($copy->status);
599
600         my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
601         my ($stat) = $meth->run( $login_session, $trans, $session );
602         if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
603         else { $copy->status(6); } #status in transit 
604 }
605
606
607
608 __PACKAGE__->register_method(
609         method  => "create_hold_transit",
610         api_name        => "open-ils.circ.hold_transit.create",
611         notes           => <<"  NOTE");
612         Creates a new transit object
613         NOTE
614
615 sub create_hold_transit {
616         my( $self, $client, $login_session, $transit, $session ) = @_;
617
618         my( $user, $evt ) = $apputils->checkses($login_session);
619         return $evt if $evt;
620         $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
621         return $evt if $evt;
622
623         my $ses;
624         if($session) { $ses = $session; } 
625         else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
626
627         return $ses->request(
628                 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
629 }
630
631 =cut
632
633
634 sub find_local_hold {
635         my( $class, $session, $copy, $user ) = @_;
636         return $class->find_nearest_permitted_hold($session, $copy, $user);
637 }
638
639
640
641
642
643
644 sub fetch_open_hold_by_current_copy {
645         my $class = shift;
646         my $copyid = shift;
647         my $hold = $apputils->simplereq(
648                 'open-ils.cstore', 
649                 'open-ils.cstore.direct.action.hold_request.search.atomic',
650                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
651         return $hold->[0] if ref($hold);
652         return undef;
653 }
654
655 sub fetch_related_holds {
656         my $class = shift;
657         my $copyid = shift;
658         return $apputils->simplereq(
659                 'open-ils.cstore', 
660                 'open-ils.cstore.direct.action.hold_request.search.atomic',
661                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
662 }
663
664
665 __PACKAGE__->register_method (
666         method          => "hold_pull_list",
667         api_name                => "open-ils.circ.hold_pull_list.retrieve",
668         signature       => q/
669                 Returns a list of hold ID's that need to be "pulled"
670                 by a given location
671         /
672 );
673
674 sub hold_pull_list {
675         my( $self, $conn, $authtoken, $limit, $offset ) = @_;
676         my( $reqr, $evt ) = $U->checkses($authtoken);
677         return $evt if $evt;
678
679         my $org = $reqr->ws_ou || $reqr->home_ou;
680         # the perm locaiton shouldn't really matter here since holds
681         # will exist all over and VIEW_HOLDS should be universal
682         $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
683         return $evt if $evt;
684
685         return $U->storagereq(
686                 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
687                 $org, $limit, $offset ); 
688 }
689
690 __PACKAGE__->register_method (
691         method          => 'fetch_hold_notify',
692         api_name                => 'open-ils.circ.hold_notification.retrieve_by_hold',
693         signature       => q/ 
694                 Returns a list of hold notification objects based on hold id.
695                 @param authtoken The loggin session key
696                 @param holdid The id of the hold whose notifications we want to retrieve
697                 @return An array of hold notification objects, event on error.
698         /
699 );
700
701 sub fetch_hold_notify {
702         my( $self, $conn, $authtoken, $holdid ) = @_;
703         my( $requestor, $evt ) = $U->checkses($authtoken);
704         return $evt if $evt;
705         my ($hold, $patron);
706         ($hold, $evt) = $U->fetch_hold($holdid);
707         return $evt if $evt;
708         ($patron, $evt) = $U->fetch_user($hold->usr);
709         return $evt if $evt;
710
711         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
712         return $evt if $evt;
713
714         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
715         return $U->cstorereq(
716                 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
717 }
718
719
720 __PACKAGE__->register_method (
721         method          => 'create_hold_notify',
722         api_name                => 'open-ils.circ.hold_notification.create',
723         signature       => q/
724                 Creates a new hold notification object
725                 @param authtoken The login session key
726                 @param notification The hold notification object to create
727                 @return ID of the new object on success, Event on error
728                 /
729 );
730 sub create_hold_notify {
731         my( $self, $conn, $authtoken, $notification ) = @_;
732         my( $requestor, $evt ) = $U->checkses($authtoken);
733         return $evt if $evt;
734         my ($hold, $patron);
735         ($hold, $evt) = $U->fetch_hold($notification->hold);
736         return $evt if $evt;
737         ($patron, $evt) = $U->fetch_user($hold->usr);
738         return $evt if $evt;
739
740         # XXX perm depth probably doesn't matter here -- should always be consortium level
741         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
742         return $evt if $evt;
743
744         # Set the proper notifier 
745         $notification->notify_staff($requestor->id);
746         my $id = $U->storagereq(
747                 'open-ils.storage.direct.action.hold_notification.create', $notification );
748         return $U->DB_UPDATE_FAILED($notification) unless $id;
749         $logger->info("User ".$requestor->id." successfully created new hold notification $id");
750         return $id;
751 }
752
753
754 __PACKAGE__->register_method(
755         method  => 'reset_hold',
756         api_name        => 'open-ils.circ.hold.reset',
757         signature       => q/
758                 Un-captures and un-targets a hold, essentially returning
759                 it to the state it was in directly after it was placed,
760                 then attempts to re-target the hold
761                 @param authtoken The login session key
762                 @param holdid The id of the hold
763         /
764 );
765
766
767 sub reset_hold {
768         my( $self, $conn, $auth, $holdid ) = @_;
769         my $reqr;
770         my ($hold, $evt) = $U->fetch_hold($holdid);
771         return $evt if $evt;
772         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
773         return $evt if $evt;
774         $evt = $self->_reset_hold($reqr, $hold);
775         return $evt if $evt;
776         return 1;
777 }
778
779 sub _reset_hold {
780         my ($self, $reqr, $hold) = @_;
781
782         my $e = new_editor(xact =>1, requestor => $reqr);
783
784         if( $hold->capture_time and $hold->current_copy ) {
785
786                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
787                         or return $e->event;
788
789                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
790                         $logger->info("setting copy to status 'reshelving' on hold retarget");
791                         $copy->status(OILS_COPY_STATUS_RESHELVING);
792                         $copy->editor($e->requestor->id);
793                         $copy->edit_date('now');
794                         $e->update_asset_copy($copy) or return $e->event;
795                 }
796         }
797
798         $hold->clear_capture_time;
799         $hold->clear_current_copy;
800
801         $e->update_action_hold_request($hold) or return $e->event;
802
803         $e->commit;
804
805         $U->storagereq(
806                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
807
808         return undef;
809 }
810
811
812 __PACKAGE__->register_method(
813         method => 'fetch_open_title_holds',
814         api_name        => 'open-ils.circ.open_holds.retrieve',
815         signature       => q/
816                 Returns a list ids of un-fulfilled holds for a given title id
817                 @param authtoken The login session key
818                 @param id the id of the item whose holds we want to retrieve
819                 @param type The hold type - M, T, V, C
820         /
821 );
822
823 sub fetch_open_title_holds {
824         my( $self, $conn, $auth, $id, $type, $org ) = @_;
825         my $e = new_editor( authtoken => $auth );
826         return $e->event unless $e->checkauth;
827
828         $type ||= "T";
829         $org ||= $e->requestor->ws_ou;
830
831 #       return $e->search_action_hold_request(
832 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
833
834         # XXX make me return IDs in the future ^--
835         my $holds = $e->search_action_hold_request(
836                 { 
837                         target                          => $id, 
838                         cancel_time                     => undef, 
839                         hold_type                       => $type, 
840                         fulfillment_time        => undef 
841                 }
842         );
843
844         flesh_hold_transits($holds);
845         return $holds;
846 }
847
848
849 sub flesh_hold_transits {
850         my $holds = shift;
851         for my $hold ( @$holds ) {
852                 $hold->transit(
853                         $apputils->simplereq(
854                                 'open-ils.cstore',
855                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
856                                 { hold => $hold->id },
857                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
858                         )->[0]
859                 );
860         }
861 }
862
863
864
865
866 __PACKAGE__->register_method(
867         method => 'fetch_captured_holds',
868         api_name        => 'open-ils.circ.captured_holds.on_shelf.retrieve',
869         signature       => q/
870                 Returns a list ids of un-fulfilled holds for a given title id
871                 @param authtoken The login session key
872                 @param org The org id of the location in question
873         /
874 );
875 sub fetch_captured_holds {
876         my( $self, $conn, $auth, $org ) = @_;
877
878         my $e = new_editor(authtoken => $auth);
879         return $e->event unless $e->checkauth;
880         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
881
882         $org ||= $e->requestor->ws_ou;
883
884         my $holds = $e->search_action_hold_request(
885                 { 
886                         capture_time            => { "!=" => undef },
887                         current_copy            => { "!=" => undef },
888                         fulfillment_time        => undef,
889                         pickup_lib                      => $org,
890                         cancel_time                     => undef,
891                 }
892         );
893
894         my @res;
895         for my $h (@$holds) {
896                 my $copy = $e->retrieve_asset_copy($h->current_copy)
897                         or return $e->event;
898                 push( @res, $h ) if 
899                         $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
900         }
901
902         flesh_hold_transits(\@res);
903         return \@res;
904 }
905
906
907
908
909
910 __PACKAGE__->register_method(
911         method  => "check_title_hold",
912         api_name        => "open-ils.circ.title_hold.is_possible",
913         notes           => q/
914                 Determines if a hold were to be placed by a given user,
915                 whether or not said hold would have any potential copies
916                 to fulfill it.
917                 @param authtoken The login session key
918                 @param params A hash of named params including:
919                         patronid  - the id of the hold recipient
920                         titleid (brn) - the id of the title to be held
921                         depth   - the hold range depth (defaults to 0)
922         /);
923
924 sub check_title_hold {
925         my( $self, $client, $authtoken, $params ) = @_;
926
927         my %params              = %$params;
928         my $titleid             = $params{titleid} ||"";
929         my $mrid                        = $params{mrid} ||"";
930         my $depth               = $params{depth} || 0;
931         my $pickup_lib  = $params{pickup_lib};
932         my $hold_type   = $params{hold_type} || 'T';
933
934         my $e = new_editor(authtoken=>$authtoken);
935         return $e->event unless $e->checkauth;
936         my $patron = $e->retrieve_actor_user($params{patronid})
937                 or return $e->event;
938
939         if( $e->requestor->id ne $patron->id ) {
940                 return $e->event unless 
941                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
942         }
943
944         return OpenILS::Event->new('PATRON_BARRED') 
945                 if $patron->barred and 
946                         ($patron->barred =~ /t/i or $patron->barred == 1);
947
948         my $rangelib    = $params{range_lib} || $patron->home_ou;
949
950         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
951                 or return $e->event;
952
953         if( $hold_type eq 'T' ) {
954                 return _check_title_hold_is_possible(
955                         $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
956         }
957
958         if( $hold_type eq 'M' ) {
959                 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
960                 my @recs = map { $_->source } @$maps;
961                 for my $rec (@recs) {
962                         return 1 if (_check_title_hold_is_possible(
963                                 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
964                 }
965         }
966 }
967
968
969
970 sub _check_title_hold_is_possible {
971         my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
972
973         my $limit       = 10;
974         my $offset      = 0;
975         my $title;
976
977         $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
978
979         while( $title = $U->storagereq(
980                                 'open-ils.storage.biblio.record_entry.ranged_tree', 
981                                 $titleid, $rangelib, $depth, $limit, $offset ) ) {
982
983                 last unless 
984                         ref($title) and 
985                         ref($title->call_numbers) and 
986                         @{$title->call_numbers};
987
988                 for my $cn (@{$title->call_numbers}) {
989         
990                         $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
991         
992                         for my $copy (@{$cn->copies}) {
993         
994                                 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
995         
996                                 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
997                                         {       patron                          => $patron, 
998                                                 requestor                       => $requestor, 
999                                                 copy                                    => $copy,
1000                                                 title                                   => $title, 
1001                                                 title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1002                                                 pickup_lib                      => $pickup_lib,
1003                                                 request_lib                     => $request_lib 
1004                                         } 
1005                                 );
1006         
1007                                 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1008                         }
1009                 }
1010
1011                 $offset += $limit;
1012         }
1013         return 0;
1014 }
1015
1016
1017
1018 sub find_nearest_permitted_hold {
1019
1020         my $class       = shift;
1021         my $session = shift;
1022         my $copy                = shift;
1023         my $user                = shift;
1024         my $evt         = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1025
1026         # first see if this copy has already been selected to fulfill a hold
1027         my $hold  = $session->request(
1028                 "open-ils.storage.direct.action.hold_request.search_where",
1029                 { current_copy => $copy->id, cancel_time => undef, capture_time => undef } )->gather(1);
1030
1031         if( $hold ) {
1032                 $logger->info("hold found which can be fulfilled by copy ".$copy->id);
1033                 return $hold;
1034         }
1035
1036         # We know this hold is permitted, so just return it
1037         return $hold if $hold;
1038
1039         $logger->debug("searching for potential holds at org ". 
1040                 $user->ws_ou." and copy ".$copy->id);
1041
1042         my $holds = $session->request(
1043                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1044                 $user->ws_ou, $copy->id, 5 )->gather(1);
1045
1046         return (undef, $evt) unless @$holds;
1047
1048         # for each potential hold, we have to run the permit script
1049         # to make sure the hold is actually permitted.
1050
1051         for my $holdid (@$holds) {
1052                 next unless $holdid;
1053                 $logger->info("Checking if hold $holdid is permitted for user ".$user->id);
1054
1055                 my ($hold) = $U->fetch_hold($holdid);
1056                 next unless $hold;
1057                 my ($reqr) = $U->fetch_user($hold->requestor);
1058
1059                 return ($hold) if OpenILS::Utils::PermitHold::permit_copy_hold(
1060                         {
1061                                 patron_id                       => $hold->usr,
1062                                 requestor                       => $reqr->id,
1063                                 copy                                    => $copy,
1064                                 pickup_lib                      => $hold->pickup_lib,
1065                                 request_lib                     => $hold->request_lib 
1066                         } 
1067                 );
1068         }
1069
1070         return (undef, $evt);
1071 }
1072
1073
1074 1;