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