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