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