f91b5e682d71eea711a9a26ba70b314337751699
[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, $login_session, $hold_id) = @_;
289
290         #FIXME
291         #my( $user, $target, $evt ) = $apputils->checkses_requestor(
292         #       $login_session, $user_id, 'VIEW_HOLD' );
293         #return $evt if $evt;
294
295         my $holds = $apputils->simplereq(
296                 'open-ils.cstore',
297                 "open-ils.cstore.direct.action.hold_request.search.atomic",
298                 { id =>  $hold_id , fulfillment_time => undef }, 
299                 { order_by => { ahr => "request_time" } }
300         );
301
302         flesh_hold_transits($holds);
303         return $holds;
304 }
305
306
307 __PACKAGE__->register_method(
308         method  => "retrieve_holds",
309         api_name        => "open-ils.circ.holds.retrieve",
310         notes           => <<NOTE);
311 Retrieves all the holds, with hold transits attached, for the specified
312 user id.  The login session is the requestor and if the requestor is
313 different from the user, then the requestor must have VIEW_HOLD permissions.
314 NOTE
315
316
317 sub retrieve_holds {
318         my($self, $client, $login_session, $user_id) = @_;
319
320         my( $user, $target, $evt ) = $apputils->checkses_requestor(
321                 $login_session, $user_id, 'VIEW_HOLD' );
322         return $evt if $evt;
323
324         my $holds = $apputils->simplereq(
325                 'open-ils.cstore',
326                 "open-ils.cstore.direct.action.hold_request.search.atomic",
327                 { 
328                         usr =>  $user_id , 
329                         fulfillment_time => undef,
330                         cancel_time => undef,
331                 }, 
332                 { order_by => { ahr => "request_time" } }
333         );
334         
335         for my $hold ( @$holds ) {
336                 $hold->transit(
337                         $apputils->simplereq(
338                                 'open-ils.cstore',
339                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
340                                 { hold => $hold->id },
341                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
342                         )->[0]
343                 );
344         }
345
346         return $holds;
347 }
348
349 __PACKAGE__->register_method(
350         method  => "retrieve_holds_by_pickup_lib",
351         api_name        => "open-ils.circ.holds.retrieve_by_pickup_lib",
352         notes           => <<NOTE);
353 Retrieves all the holds, with hold transits attached, for the specified
354 pickup_ou id. 
355 NOTE
356
357
358 sub retrieve_holds_by_pickup_lib {
359         my($self, $client, $login_session, $ou_id) = @_;
360
361         #FIXME -- put an appropriate permission check here
362         #my( $user, $target, $evt ) = $apputils->checkses_requestor(
363         #       $login_session, $user_id, 'VIEW_HOLD' );
364         #return $evt if $evt;
365
366         my $holds = $apputils->simplereq(
367                 'open-ils.cstore',
368                 "open-ils.cstore.direct.action.hold_request.search.atomic",
369                 { 
370                         pickup_lib =>  $ou_id , 
371                         fulfillment_time => undef,
372                         cancel_time => undef
373                 }, 
374                 { order_by => { ahr => "request_time" } });
375
376
377         flesh_hold_transits($holds);
378         return $holds;
379 }
380
381
382 __PACKAGE__->register_method(
383         method  => "cancel_hold",
384         api_name        => "open-ils.circ.hold.cancel",
385         notes           => <<"  NOTE");
386         Cancels the specified hold.  The login session
387         is the requestor and if the requestor is different from the usr field
388         on the hold, the requestor must have CANCEL_HOLDS permissions.
389         the hold may be either the hold object or the hold id
390         NOTE
391
392 sub cancel_hold {
393         my($self, $client, $auth, $holdid) = @_;
394
395         my $e = new_editor(authtoken=>$auth, xact=>1);
396         return $e->event unless $e->checkauth;
397
398         my $hold = $e->retrieve_action_hold_request($holdid)
399                 or return $e->event;
400
401         if( $e->requestor->id ne $hold->usr ) {
402                 return $e->event unless $e->allowed('CANCEL_HOLDS');
403         }
404
405         return 1 if $hold->cancel_time;
406
407         # If the hold is captured, reset the copy status
408         if( $hold->capture_time and $hold->current_copy ) {
409
410                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
411                         or return $e->event;
412
413                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
414                         $logger->info("setting copy to status 'reshelving' on hold cancel");
415                         $copy->status(OILS_COPY_STATUS_RESHELVING);
416                         $copy->editor($e->requestor->id);
417                         $copy->edit_date('now');
418                         $e->update_asset_copy($copy) or return $e->event;
419                 }
420         }
421
422         $hold->cancel_time('now');
423         $e->update_action_hold_request($hold)
424                 or return $e->event;
425
426         $self->delete_hold_copy_maps($e, $hold->id);
427
428         $e->commit;
429         return 1;
430 }
431
432 sub delete_hold_copy_maps {
433         my $class = shift;
434         my $editor = shift;
435         my $holdid = shift;
436
437         my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
438         for(@$maps) {
439                 $editor->delete_action_hold_copy_map($_) 
440                         or return $editor->event;
441         }
442         return undef;
443 }
444
445
446 __PACKAGE__->register_method(
447         method  => "update_hold",
448         api_name        => "open-ils.circ.hold.update",
449         notes           => <<"  NOTE");
450         Updates the specified hold.  The login session
451         is the requestor and if the requestor is different from the usr field
452         on the hold, the requestor must have UPDATE_HOLDS permissions.
453         NOTE
454
455 sub update_hold {
456         my($self, $client, $login_session, $hold) = @_;
457
458         my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
459                 $login_session, $hold->usr, 'UPDATE_HOLD' );
460         return $evt if $evt;
461
462         $logger->activity('User ' . $requestor->id . 
463                 ' updating hold ' . $hold->id . ' for user ' . $target->id );
464
465         return $U->storagereq(
466                 "open-ils.storage.direct.action.hold_request.update", $hold );
467 }
468
469
470 __PACKAGE__->register_method(
471         method  => "retrieve_hold_status",
472         api_name        => "open-ils.circ.hold.status.retrieve",
473         notes           => <<"  NOTE");
474         Calculates the current status of the hold.
475         the requestor must have VIEW_HOLD permissions if the hold is for a user
476         other than the requestor.
477         Returns -1  on error (for now)
478         Returns 1 for 'waiting for copy to become available'
479         Returns 2 for 'waiting for copy capture'
480         Returns 3 for 'in transit'
481         Returns 4 for 'arrived'
482         NOTE
483
484 sub retrieve_hold_status {
485         my($self, $client, $auth, $hold_id) = @_;
486
487         my $e = new_editor(authtoken => $auth);
488         return $e->event unless $e->checkauth;
489         my $hold = $e->retrieve_action_hold_request($hold_id)
490                 or return $e->event;
491
492         if( $e->requestor->id != $hold->usr ) {
493                 return $e->event unless $e->allowed('VIEW_HOLD');
494         }
495
496         return 1 unless $hold->current_copy;
497         return 2 unless $hold->capture_time;
498
499         my $copy = $e->retrieve_asset_copy($hold->current_copy)
500                 or return $e->event;
501
502         return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
503         return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
504
505         return -1;
506 }
507
508
509
510
511
512 =head DEPRECATED
513 __PACKAGE__->register_method(
514         method  => "capture_copy",
515         api_name        => "open-ils.circ.hold.capture_copy.barcode",
516         notes           => <<"  NOTE");
517         Captures a copy to fulfil a hold
518         Params is login session and copy barcode
519         Optional param is 'flesh'.  If set, we also return the
520         relevant copy and title
521         login mus have COPY_CHECKIN permissions (since this is essentially
522         copy checkin)
523         NOTE
524
525 # XXX deprecate me XXX
526
527 sub capture_copy {
528         my( $self, $client, $login_session, $params ) = @_;
529         my %params = %$params;
530         my $barcode = $params{barcode};
531
532
533         my( $user, $target, $copy, $hold, $evt );
534
535         ( $user, $evt ) = $apputils->checkses($login_session);
536         return $evt if $evt;
537
538         # am I allowed to checkin a copy?
539         $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
540         return $evt if $evt;
541
542         $logger->info("Capturing copy with barcode $barcode");
543
544         my $session = $apputils->start_db_session();
545
546         ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
547         return $evt if $evt;
548
549         $logger->debug("Capturing copy " . $copy->id);
550
551         #( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
552         ( $hold, $evt ) = $self->find_nearest_permitted_hold($session, $copy, $user);
553         return $evt if $evt;
554
555         warn "Found hold " . $hold->id . "\n";
556         $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
557
558         $hold->current_copy($copy->id);
559         $hold->capture_time("now"); 
560
561         #update the hold
562         my $stat = $session->request(
563                         "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
564         if(!$stat) { throw OpenSRF::EX::ERROR 
565                 ("Error updating hold request " . $copy->id); }
566
567         $copy->status(OILS_COPY_STATUS_ON_HOLDS_SHELF); #status on holds shelf
568
569         # if the staff member capturing this item is not at the pickup lib
570         if( $user->home_ou ne $hold->pickup_lib ) {
571                 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
572         }
573
574         $copy->editor($user->id);
575         $copy->edit_date("now");
576         $stat = $session->request(
577                 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
578         if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
579
580         my $payload = { hold => $hold };
581         $payload->{copy} = $copy if $params{flesh_copy};
582
583         if($params{flesh_record}) {
584                 my $record;
585                 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
586                 return $evt if $evt;
587                 $record = $apputils->record_to_mvr($record);
588                 $payload->{record} = $record;
589         }
590
591         $apputils->commit_db_session($session);
592
593         return OpenILS::Event->new('ROUTE_ITEM', 
594                 route_to => $hold->pickup_lib, payload => $payload );
595 }
596
597 sub _build_hold_transit {
598         my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
599         my $trans = Fieldmapper::action::hold_transit_copy->new;
600
601         $trans->hold($hold->id);
602         $trans->source($user->home_ou);
603         $trans->dest($hold->pickup_lib);
604         $trans->source_send_time("now");
605         $trans->target_copy($copy->id);
606         $trans->copy_status($copy->status);
607
608         my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
609         my ($stat) = $meth->run( $login_session, $trans, $session );
610         if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
611         else { $copy->status(6); } #status in transit 
612 }
613
614
615
616 __PACKAGE__->register_method(
617         method  => "create_hold_transit",
618         api_name        => "open-ils.circ.hold_transit.create",
619         notes           => <<"  NOTE");
620         Creates a new transit object
621         NOTE
622
623 sub create_hold_transit {
624         my( $self, $client, $login_session, $transit, $session ) = @_;
625
626         my( $user, $evt ) = $apputils->checkses($login_session);
627         return $evt if $evt;
628         $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
629         return $evt if $evt;
630
631         my $ses;
632         if($session) { $ses = $session; } 
633         else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
634
635         return $ses->request(
636                 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
637 }
638
639 =cut
640
641
642 sub find_local_hold {
643         my( $class, $session, $copy, $user ) = @_;
644         return $class->find_nearest_permitted_hold($session, $copy, $user);
645 }
646
647
648
649
650
651
652 sub fetch_open_hold_by_current_copy {
653         my $class = shift;
654         my $copyid = shift;
655         my $hold = $apputils->simplereq(
656                 'open-ils.cstore', 
657                 'open-ils.cstore.direct.action.hold_request.search.atomic',
658                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
659         return $hold->[0] if ref($hold);
660         return undef;
661 }
662
663 sub fetch_related_holds {
664         my $class = shift;
665         my $copyid = shift;
666         return $apputils->simplereq(
667                 'open-ils.cstore', 
668                 'open-ils.cstore.direct.action.hold_request.search.atomic',
669                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
670 }
671
672
673 __PACKAGE__->register_method (
674         method          => "hold_pull_list",
675         api_name                => "open-ils.circ.hold_pull_list.retrieve",
676         signature       => q/
677                 Returns a list of hold ID's that need to be "pulled"
678                 by a given location
679         /
680 );
681
682 sub hold_pull_list {
683         my( $self, $conn, $authtoken, $limit, $offset ) = @_;
684         my( $reqr, $evt ) = $U->checkses($authtoken);
685         return $evt if $evt;
686
687         my $org = $reqr->ws_ou || $reqr->home_ou;
688         # the perm locaiton shouldn't really matter here since holds
689         # will exist all over and VIEW_HOLDS should be universal
690         $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
691         return $evt if $evt;
692
693         return $U->storagereq(
694                 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
695                 $org, $limit, $offset ); 
696 }
697
698 __PACKAGE__->register_method (
699         method          => 'fetch_hold_notify',
700         api_name                => 'open-ils.circ.hold_notification.retrieve_by_hold',
701         signature       => q/ 
702                 Returns a list of hold notification objects based on hold id.
703                 @param authtoken The loggin session key
704                 @param holdid The id of the hold whose notifications we want to retrieve
705                 @return An array of hold notification objects, event on error.
706         /
707 );
708
709 sub fetch_hold_notify {
710         my( $self, $conn, $authtoken, $holdid ) = @_;
711         my( $requestor, $evt ) = $U->checkses($authtoken);
712         return $evt if $evt;
713         my ($hold, $patron);
714         ($hold, $evt) = $U->fetch_hold($holdid);
715         return $evt if $evt;
716         ($patron, $evt) = $U->fetch_user($hold->usr);
717         return $evt if $evt;
718
719         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
720         return $evt if $evt;
721
722         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
723         return $U->cstorereq(
724                 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
725 }
726
727
728 __PACKAGE__->register_method (
729         method          => 'create_hold_notify',
730         api_name                => 'open-ils.circ.hold_notification.create',
731         signature       => q/
732                 Creates a new hold notification object
733                 @param authtoken The login session key
734                 @param notification The hold notification object to create
735                 @return ID of the new object on success, Event on error
736                 /
737 );
738 sub create_hold_notify {
739         my( $self, $conn, $authtoken, $notification ) = @_;
740         my( $requestor, $evt ) = $U->checkses($authtoken);
741         return $evt if $evt;
742         my ($hold, $patron);
743         ($hold, $evt) = $U->fetch_hold($notification->hold);
744         return $evt if $evt;
745         ($patron, $evt) = $U->fetch_user($hold->usr);
746         return $evt if $evt;
747
748         # XXX perm depth probably doesn't matter here -- should always be consortium level
749         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
750         return $evt if $evt;
751
752         # Set the proper notifier 
753         $notification->notify_staff($requestor->id);
754         my $id = $U->storagereq(
755                 'open-ils.storage.direct.action.hold_notification.create', $notification );
756         return $U->DB_UPDATE_FAILED($notification) unless $id;
757         $logger->info("User ".$requestor->id." successfully created new hold notification $id");
758         return $id;
759 }
760
761
762 __PACKAGE__->register_method(
763         method  => 'reset_hold',
764         api_name        => 'open-ils.circ.hold.reset',
765         signature       => q/
766                 Un-captures and un-targets a hold, essentially returning
767                 it to the state it was in directly after it was placed,
768                 then attempts to re-target the hold
769                 @param authtoken The login session key
770                 @param holdid The id of the hold
771         /
772 );
773
774
775 sub reset_hold {
776         my( $self, $conn, $auth, $holdid ) = @_;
777         my $reqr;
778         my ($hold, $evt) = $U->fetch_hold($holdid);
779         return $evt if $evt;
780         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
781         return $evt if $evt;
782         $evt = $self->_reset_hold($reqr, $hold);
783         return $evt if $evt;
784         return 1;
785 }
786
787 sub _reset_hold {
788         my ($self, $reqr, $hold) = @_;
789
790         my $e = new_editor(xact =>1, requestor => $reqr);
791
792         $logger->info("reseting hold ".$hold->id);
793
794         if( $hold->capture_time and $hold->current_copy ) {
795
796                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
797                         or return $e->event;
798
799                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
800                         $logger->info("setting copy to status 'reshelving' on hold retarget");
801                         $copy->status(OILS_COPY_STATUS_RESHELVING);
802                         $copy->editor($e->requestor->id);
803                         $copy->edit_date('now');
804                         $e->update_asset_copy($copy) or return $e->event;
805
806                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
807                         $logger->warn("reseting hold that is in transit: ".$hold->id);
808                         # is this allowed?      
809                 }
810         }
811
812         $hold->clear_capture_time;
813         $hold->clear_current_copy;
814
815         $e->update_action_hold_request($hold) or return $e->event;
816
817         $e->commit;
818
819         $U->storagereq(
820                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
821
822         return undef;
823 }
824
825
826 __PACKAGE__->register_method(
827         method => 'fetch_open_title_holds',
828         api_name        => 'open-ils.circ.open_holds.retrieve',
829         signature       => q/
830                 Returns a list ids of un-fulfilled holds for a given title id
831                 @param authtoken The login session key
832                 @param id the id of the item whose holds we want to retrieve
833                 @param type The hold type - M, T, V, C
834         /
835 );
836
837 sub fetch_open_title_holds {
838         my( $self, $conn, $auth, $id, $type, $org ) = @_;
839         my $e = new_editor( authtoken => $auth );
840         return $e->event unless $e->checkauth;
841
842         $type ||= "T";
843         $org ||= $e->requestor->ws_ou;
844
845 #       return $e->search_action_hold_request(
846 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
847
848         # XXX make me return IDs in the future ^--
849         my $holds = $e->search_action_hold_request(
850                 { 
851                         target                          => $id, 
852                         cancel_time                     => undef, 
853                         hold_type                       => $type, 
854                         fulfillment_time        => undef 
855                 }
856         );
857
858         flesh_hold_transits($holds);
859         return $holds;
860 }
861
862
863 sub flesh_hold_transits {
864         my $holds = shift;
865         for my $hold ( @$holds ) {
866                 $hold->transit(
867                         $apputils->simplereq(
868                                 'open-ils.cstore',
869                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
870                                 { hold => $hold->id },
871                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
872                         )->[0]
873                 );
874         }
875 }
876
877
878
879
880 __PACKAGE__->register_method(
881         method => 'fetch_captured_holds',
882         api_name        => 'open-ils.circ.captured_holds.on_shelf.retrieve',
883         signature       => q/
884                 Returns a list ids of un-fulfilled holds for a given title id
885                 @param authtoken The login session key
886                 @param org The org id of the location in question
887         /
888 );
889 sub fetch_captured_holds {
890         my( $self, $conn, $auth, $org ) = @_;
891
892         my $e = new_editor(authtoken => $auth);
893         return $e->event unless $e->checkauth;
894         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
895
896         $org ||= $e->requestor->ws_ou;
897
898         my $holds = $e->search_action_hold_request(
899                 { 
900                         capture_time            => { "!=" => undef },
901                         current_copy            => { "!=" => undef },
902                         fulfillment_time        => undef,
903                         pickup_lib                      => $org,
904                         cancel_time                     => undef,
905                 }
906         );
907
908         my @res;
909         for my $h (@$holds) {
910                 my $copy = $e->retrieve_asset_copy($h->current_copy)
911                         or return $e->event;
912                 push( @res, $h ) if 
913                         $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
914         }
915
916         flesh_hold_transits(\@res);
917         return \@res;
918 }
919
920
921
922
923
924 __PACKAGE__->register_method(
925         method  => "check_title_hold",
926         api_name        => "open-ils.circ.title_hold.is_possible",
927         notes           => q/
928                 Determines if a hold were to be placed by a given user,
929                 whether or not said hold would have any potential copies
930                 to fulfill it.
931                 @param authtoken The login session key
932                 @param params A hash of named params including:
933                         patronid  - the id of the hold recipient
934                         titleid (brn) - the id of the title to be held
935                         depth   - the hold range depth (defaults to 0)
936         /);
937
938 sub check_title_hold {
939         my( $self, $client, $authtoken, $params ) = @_;
940
941         my %params              = %$params;
942         my $titleid             = $params{titleid} ||"";
943         my $mrid                        = $params{mrid} ||"";
944         my $depth               = $params{depth} || 0;
945         my $pickup_lib  = $params{pickup_lib};
946         my $hold_type   = $params{hold_type} || 'T';
947
948         my $e = new_editor(authtoken=>$authtoken);
949         return $e->event unless $e->checkauth;
950         my $patron = $e->retrieve_actor_user($params{patronid})
951                 or return $e->event;
952
953         if( $e->requestor->id ne $patron->id ) {
954                 return $e->event unless 
955                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
956         }
957
958         return OpenILS::Event->new('PATRON_BARRED') 
959                 if $patron->barred and 
960                         ($patron->barred =~ /t/i or $patron->barred == 1);
961
962         my $rangelib    = $params{range_lib} || $patron->home_ou;
963
964         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
965                 or return $e->event;
966
967         if( $hold_type eq 'T' ) {
968                 return _check_title_hold_is_possible(
969                         $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
970         }
971
972         if( $hold_type eq 'M' ) {
973                 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
974                 my @recs = map { $_->source } @$maps;
975                 for my $rec (@recs) {
976                         return 1 if (_check_title_hold_is_possible(
977                                 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
978                 }
979         }
980 }
981
982
983
984 sub _check_title_hold_is_possible {
985         my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
986
987         my $limit       = 10;
988         my $offset      = 0;
989         my $title;
990
991         $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
992
993         while( $title = $U->storagereq(
994                                 'open-ils.storage.biblio.record_entry.ranged_tree', 
995                                 $titleid, $rangelib, $depth, $limit, $offset ) ) {
996
997                 last unless 
998                         ref($title) and 
999                         ref($title->call_numbers) and 
1000                         @{$title->call_numbers};
1001
1002                 for my $cn (@{$title->call_numbers}) {
1003         
1004                         $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1005         
1006                         for my $copy (@{$cn->copies}) {
1007         
1008                                 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1009         
1010                                 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1011                                         {       patron                          => $patron, 
1012                                                 requestor                       => $requestor, 
1013                                                 copy                                    => $copy,
1014                                                 title                                   => $title, 
1015                                                 title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1016                                                 pickup_lib                      => $pickup_lib,
1017                                                 request_lib                     => $request_lib 
1018                                         } 
1019                                 );
1020         
1021                                 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1022                         }
1023                 }
1024
1025                 $offset += $limit;
1026         }
1027         return 0;
1028 }
1029
1030
1031
1032 sub find_nearest_permitted_hold {
1033
1034         my $class       = shift;
1035         my $session = shift;
1036         my $copy                = shift;
1037         my $user                = shift;
1038         my $evt         = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1039
1040         # first see if this copy has already been selected to fulfill a hold
1041         my $hold  = $session->request(
1042                 "open-ils.storage.direct.action.hold_request.search_where",
1043                 { current_copy => $copy->id, cancel_time => undef, capture_time => undef } )->gather(1);
1044
1045         if( $hold ) {
1046                 $logger->info("hold found which can be fulfilled by copy ".$copy->id);
1047                 return $hold;
1048         }
1049
1050         # We know this hold is permitted, so just return it
1051         return $hold if $hold;
1052
1053         $logger->debug("searching for potential holds at org ". 
1054                 $user->ws_ou." and copy ".$copy->id);
1055
1056         my $holds = $session->request(
1057                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1058                 $user->ws_ou, $copy->id, 5 )->gather(1);
1059
1060         return (undef, $evt) unless @$holds;
1061
1062         # for each potential hold, we have to run the permit script
1063         # to make sure the hold is actually permitted.
1064
1065         for my $holdid (@$holds) {
1066                 next unless $holdid;
1067                 $logger->info("Checking if hold $holdid is permitted for user ".$user->id);
1068
1069                 my ($hold) = $U->fetch_hold($holdid);
1070                 next unless $hold;
1071                 my ($reqr) = $U->fetch_user($hold->requestor);
1072
1073                 return ($hold) if OpenILS::Utils::PermitHold::permit_copy_hold(
1074                         {
1075                                 patron_id                       => $hold->usr,
1076                                 requestor                       => $reqr->id,
1077                                 copy                                    => $copy,
1078                                 pickup_lib                      => $hold->pickup_lib,
1079                                 request_lib                     => $hold->request_lib 
1080                         } 
1081                 );
1082         }
1083
1084         return (undef, $evt);
1085 }
1086
1087
1088 __PACKAGE__->register_method(
1089         method => 'all_rec_holds',
1090         api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1091 );
1092
1093 sub all_rec_holds {
1094         my( $self, $conn, $auth, $title_id, $args ) = @_;
1095
1096         my $e = new_editor(authtoken=>$auth);
1097         $e->checkauth or return $e->event;
1098         $e->allowed('VIEW_HOLD') or return $e->event;
1099
1100         $args ||= { fulfillment_time => undef };
1101         $args->{cancel_time} = undef;
1102
1103         my $resp = {};
1104
1105         $resp->{title_holds} = $e->search_action_hold_request(
1106                 { 
1107                         hold_type => OILS_HOLD_TYPE_TITLE, 
1108                         target => $title_id, 
1109                         %$args 
1110                 }, {idlist=>1} );
1111
1112         my $vols = $e->search_asset_call_number(
1113                 { record => $title_id, deleted => 'f' }, {idlist=>1});
1114
1115         $resp->{volume_holds} = (!@$vols) ? [] : $e->search_action_hold_request(
1116                 { 
1117                         hold_type => OILS_HOLD_TYPE_VOLUME, 
1118                         target => $vols,
1119                         %$args }, 
1120                 {idlist=>1} );
1121
1122         my $copies = $e->search_asset_copy(
1123                 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1124
1125         $resp->{copy_holds} = (!@$copies) ? [] : $e->search_action_hold_request(
1126                 { 
1127                         hold_type => OILS_HOLD_TYPE_COPY,
1128                         target => $copies,
1129                         %$args }, 
1130                 {idlist=>1} );
1131
1132         return $resp;
1133 }
1134
1135
1136 1;