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