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