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