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