]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
added a MAX_HOLDS event and tests in the hold permit scripts
[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
367 __PACKAGE__->register_method(
368    method => 'user_hold_count',
369    api_name => 'open-ils.circ.hold.user.count');
370
371 sub user_hold_count {
372    my( $self, $conn, $auth, $userid ) = @_;
373    my $e = new_editor(authtoken=>$auth);
374    return $e->event unless $e->checkauth;
375    my $patron = $e->retrieve_actor_user($userid)
376       or return $e->event;
377    return $e->event unless $e->allowed('VIEW_HOLD', $patron->home_ou);
378    return $self->__user_hold_count($e, $userid);
379 }
380
381 sub __user_hold_count {
382    my( $self, $e, $userid ) = @_;
383    my $holds = $e->search_action_hold_request(
384       {  usr =>  $userid , 
385          fulfillment_time => undef,
386          cancel_time => undef,
387       }, 
388       {idlist => 1}
389    );
390
391    return scalar(@$holds);
392 }
393
394
395 __PACKAGE__->register_method(
396         method  => "retrieve_holds_by_pickup_lib",
397         api_name        => "open-ils.circ.holds.retrieve_by_pickup_lib",
398         notes           => <<NOTE);
399 Retrieves all the holds, with hold transits attached, for the specified
400 pickup_ou id. 
401 NOTE
402
403 __PACKAGE__->register_method(
404         method  => "retrieve_holds_by_pickup_lib",
405         api_name        => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
406         notes           => <<NOTE);
407 Retrieves all the hold ids for the specified
408 pickup_ou id. 
409 NOTE
410
411 sub retrieve_holds_by_pickup_lib {
412         my($self, $client, $login_session, $ou_id) = @_;
413
414         #FIXME -- put an appropriate permission check here
415         #my( $user, $target, $evt ) = $apputils->checkses_requestor(
416         #       $login_session, $user_id, 'VIEW_HOLD' );
417         #return $evt if $evt;
418
419         my $holds = $apputils->simplereq(
420                 'open-ils.cstore',
421                 "open-ils.cstore.direct.action.hold_request.search.atomic",
422                 { 
423                         pickup_lib =>  $ou_id , 
424                         fulfillment_time => undef,
425                         cancel_time => undef
426                 }, 
427                 { order_by => { ahr => "request_time" } });
428
429
430         if( ! $self->api_name =~ /id_list/ ) {
431                 flesh_hold_transits($holds);
432         }
433
434         if( $self->api_name =~ /id_list/ ) {
435                 return [ map { $_->id } @$holds ];
436         } else {
437                 return $holds;
438         }
439 }
440
441 __PACKAGE__->register_method(
442         method  => "cancel_hold",
443         api_name        => "open-ils.circ.hold.cancel",
444         notes           => <<"  NOTE");
445         Cancels the specified hold.  The login session
446         is the requestor and if the requestor is different from the usr field
447         on the hold, the requestor must have CANCEL_HOLDS permissions.
448         the hold may be either the hold object or the hold id
449         NOTE
450
451 sub cancel_hold {
452         my($self, $client, $auth, $holdid) = @_;
453
454         my $e = new_editor(authtoken=>$auth, xact=>1);
455         return $e->event unless $e->checkauth;
456
457         my $hold = $e->retrieve_action_hold_request($holdid)
458                 or return $e->event;
459
460         if( $e->requestor->id ne $hold->usr ) {
461                 return $e->event unless $e->allowed('CANCEL_HOLDS');
462         }
463
464         return 1 if $hold->cancel_time;
465
466         # If the hold is captured, reset the copy status
467         if( $hold->capture_time and $hold->current_copy ) {
468
469                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
470                         or return $e->event;
471
472                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
473          $logger->info("canceling hold $holdid whose item is on the holds shelf");
474 #                       $logger->info("setting copy to status 'reshelving' on hold cancel");
475 #                       $copy->status(OILS_COPY_STATUS_RESHELVING);
476 #                       $copy->editor($e->requestor->id);
477 #                       $copy->edit_date('now');
478 #                       $e->update_asset_copy($copy) or return $e->event;
479
480                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
481
482                         my $hid = $hold->id;
483                         $logger->warn("! canceling hold [$hid] that is in transit");
484                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
485
486                         if( $transid ) {
487                                 my $trans = $e->retrieve_action_transit_copy($transid);
488                                 # Leave the transit alive, but  set the copy status to 
489                                 # reshelving so it will be properly reshelved when it gets back home
490                                 if( $trans ) {
491                                         $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
492                                         $e->update_action_transit_copy($trans) or return $e->die_event;
493                                 }
494                         }
495                 }
496         }
497
498         $hold->cancel_time('now');
499         $e->update_action_hold_request($hold)
500                 or return $e->event;
501
502         $self->delete_hold_copy_maps($e, $hold->id);
503
504         $e->commit;
505         return 1;
506 }
507
508 sub delete_hold_copy_maps {
509         my $class = shift;
510         my $editor = shift;
511         my $holdid = shift;
512
513         my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
514         for(@$maps) {
515                 $editor->delete_action_hold_copy_map($_) 
516                         or return $editor->event;
517         }
518         return undef;
519 }
520
521
522 __PACKAGE__->register_method(
523         method  => "update_hold",
524         api_name        => "open-ils.circ.hold.update",
525         notes           => <<"  NOTE");
526         Updates the specified hold.  The login session
527         is the requestor and if the requestor is different from the usr field
528         on the hold, the requestor must have UPDATE_HOLDS permissions.
529         NOTE
530
531 sub update_hold {
532         my($self, $client, $login_session, $hold) = @_;
533
534         my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
535                 $login_session, $hold->usr, 'UPDATE_HOLD' );
536         return $evt if $evt;
537
538         $logger->activity('User ' . $requestor->id . 
539                 ' updating hold ' . $hold->id . ' for user ' . $target->id );
540
541         return $U->storagereq(
542                 "open-ils.storage.direct.action.hold_request.update", $hold );
543 }
544
545
546 __PACKAGE__->register_method(
547         method  => "retrieve_hold_status",
548         api_name        => "open-ils.circ.hold.status.retrieve",
549         notes           => <<"  NOTE");
550         Calculates the current status of the hold.
551         the requestor must have VIEW_HOLD permissions if the hold is for a user
552         other than the requestor.
553         Returns -1  on error (for now)
554         Returns 1 for 'waiting for copy to become available'
555         Returns 2 for 'waiting for copy capture'
556         Returns 3 for 'in transit'
557         Returns 4 for 'arrived'
558         NOTE
559
560 sub retrieve_hold_status {
561         my($self, $client, $auth, $hold_id) = @_;
562
563         my $e = new_editor(authtoken => $auth);
564         return $e->event unless $e->checkauth;
565         my $hold = $e->retrieve_action_hold_request($hold_id)
566                 or return $e->event;
567
568         if( $e->requestor->id != $hold->usr ) {
569                 return $e->event unless $e->allowed('VIEW_HOLD');
570         }
571
572         return _hold_status($e, $hold);
573
574 }
575
576 sub _hold_status {
577         my($e, $hold) = @_;
578         return 1 unless $hold->current_copy;
579         return 2 unless $hold->capture_time;
580
581         my $copy = $hold->current_copy;
582         unless( ref $copy ) {
583                 $copy = $e->retrieve_asset_copy($hold->current_copy)
584                         or return $e->event;
585         }
586
587         return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
588         return 4 if $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
589
590         return -1;
591 }
592
593
594 #sub find_local_hold {
595 #       my( $class, $session, $copy, $user ) = @_;
596 #       return $class->find_nearest_permitted_hold($session, $copy, $user);
597 #}
598
599
600 sub fetch_open_hold_by_current_copy {
601         my $class = shift;
602         my $copyid = shift;
603         my $hold = $apputils->simplereq(
604                 'open-ils.cstore', 
605                 'open-ils.cstore.direct.action.hold_request.search.atomic',
606                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
607         return $hold->[0] if ref($hold);
608         return undef;
609 }
610
611 sub fetch_related_holds {
612         my $class = shift;
613         my $copyid = shift;
614         return $apputils->simplereq(
615                 'open-ils.cstore', 
616                 'open-ils.cstore.direct.action.hold_request.search.atomic',
617                 { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
618 }
619
620
621 __PACKAGE__->register_method (
622         method          => "hold_pull_list",
623         api_name                => "open-ils.circ.hold_pull_list.retrieve",
624         signature       => q/
625                 Returns a list of holds that need to be "pulled"
626                 by a given location
627         /
628 );
629
630 __PACKAGE__->register_method (
631         method          => "hold_pull_list",
632         api_name                => "open-ils.circ.hold_pull_list.id_list.retrieve",
633         signature       => q/
634                 Returns a list of hold ID's that need to be "pulled"
635                 by a given location
636         /
637 );
638
639
640 sub hold_pull_list {
641         my( $self, $conn, $authtoken, $limit, $offset ) = @_;
642         my( $reqr, $evt ) = $U->checkses($authtoken);
643         return $evt if $evt;
644
645         my $org = $reqr->ws_ou || $reqr->home_ou;
646         # the perm locaiton shouldn't really matter here since holds
647         # will exist all over and VIEW_HOLDS should be universal
648         $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
649         return $evt if $evt;
650
651         if( $self->api_name =~ /id_list/ ) {
652                 return $U->storagereq(
653                         'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.atomic',
654                         $org, $limit, $offset ); 
655         } else {
656                 return $U->storagereq(
657                         'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
658                         $org, $limit, $offset ); 
659         }
660 }
661
662 __PACKAGE__->register_method (
663         method          => 'fetch_hold_notify',
664         api_name                => 'open-ils.circ.hold_notification.retrieve_by_hold',
665         signature       => q/ 
666                 Returns a list of hold notification objects based on hold id.
667                 @param authtoken The loggin session key
668                 @param holdid The id of the hold whose notifications we want to retrieve
669                 @return An array of hold notification objects, event on error.
670         /
671 );
672
673 sub fetch_hold_notify {
674         my( $self, $conn, $authtoken, $holdid ) = @_;
675         my( $requestor, $evt ) = $U->checkses($authtoken);
676         return $evt if $evt;
677         my ($hold, $patron);
678         ($hold, $evt) = $U->fetch_hold($holdid);
679         return $evt if $evt;
680         ($patron, $evt) = $U->fetch_user($hold->usr);
681         return $evt if $evt;
682
683         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
684         return $evt if $evt;
685
686         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
687         return $U->cstorereq(
688                 'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
689 }
690
691
692 __PACKAGE__->register_method (
693         method          => 'create_hold_notify',
694         api_name                => 'open-ils.circ.hold_notification.create',
695         signature       => q/
696                 Creates a new hold notification object
697                 @param authtoken The login session key
698                 @param notification The hold notification object to create
699                 @return ID of the new object on success, Event on error
700                 /
701 );
702 =head old
703 sub __create_hold_notify {
704         my( $self, $conn, $authtoken, $notification ) = @_;
705         my( $requestor, $evt ) = $U->checkses($authtoken);
706         return $evt if $evt;
707         my ($hold, $patron);
708         ($hold, $evt) = $U->fetch_hold($notification->hold);
709         return $evt if $evt;
710         ($patron, $evt) = $U->fetch_user($hold->usr);
711         return $evt if $evt;
712
713         # XXX perm depth probably doesn't matter here -- should always be consortium level
714         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
715         return $evt if $evt;
716
717         # Set the proper notifier 
718         $notification->notify_staff($requestor->id);
719         my $id = $U->storagereq(
720                 'open-ils.storage.direct.action.hold_notification.create', $notification );
721         return $U->DB_UPDATE_FAILED($notification) unless $id;
722         $logger->info("User ".$requestor->id." successfully created new hold notification $id");
723         return $id;
724 }
725 =cut
726
727 sub create_hold_notify {
728    my( $self, $conn, $auth, $note ) = @_;
729    my $e = new_editor(authtoken=>$auth, xact=>1);
730    return $e->die_event unless $e->checkauth;
731
732    my $hold = $e->retrieve_action_hold_request($note->hold)
733       or return $e->die_event;
734    my $patron = $e->retrieve_actor_user($hold->usr) 
735       or return $e->die_event;
736
737    return $e->die_event unless 
738       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
739
740         $note->notify_staff($e->requestor->id);
741    $e->create_action_hold_notification($note) or return $e->die_event;
742    $e->commit;
743    return $note->id;
744 }
745
746
747 __PACKAGE__->register_method(
748         method  => 'reset_hold',
749         api_name        => 'open-ils.circ.hold.reset',
750         signature       => q/
751                 Un-captures and un-targets a hold, essentially returning
752                 it to the state it was in directly after it was placed,
753                 then attempts to re-target the hold
754                 @param authtoken The login session key
755                 @param holdid The id of the hold
756         /
757 );
758
759
760 sub reset_hold {
761         my( $self, $conn, $auth, $holdid ) = @_;
762         my $reqr;
763         my ($hold, $evt) = $U->fetch_hold($holdid);
764         return $evt if $evt;
765         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD'); # XXX stronger permission
766         return $evt if $evt;
767         $evt = $self->_reset_hold($reqr, $hold);
768         return $evt if $evt;
769         return 1;
770 }
771
772 sub _reset_hold {
773         my ($self, $reqr, $hold) = @_;
774
775         my $e = new_editor(xact =>1, requestor => $reqr);
776
777         $logger->info("reseting hold ".$hold->id);
778
779         my $hid = $hold->id;
780
781         if( $hold->capture_time and $hold->current_copy ) {
782
783                 my $copy = $e->retrieve_asset_copy($hold->current_copy)
784                         or return $e->event;
785
786                 if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
787                         $logger->info("setting copy to status 'reshelving' on hold retarget");
788                         $copy->status(OILS_COPY_STATUS_RESHELVING);
789                         $copy->editor($e->requestor->id);
790                         $copy->edit_date('now');
791                         $e->update_asset_copy($copy) or return $e->event;
792
793                 } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
794
795                         # We don't want the copy to remain "in transit"
796                         $copy->status(OILS_COPY_STATUS_RESHELVING);
797                         $logger->warn("! reseting hold [$hid] that is in transit");
798                         my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
799
800                         if( $transid ) {
801                                 my $trans = $e->retrieve_action_transit_copy($transid);
802                                 if( $trans ) {
803                                         $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
804                                         my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
805                                         $logger->info("Transit abort completed with result $evt");
806                                         return $evt unless "$evt" eq 1;
807                                 }
808                         }
809                 }
810         }
811
812         $hold->clear_capture_time;
813         $hold->clear_current_copy;
814
815         $e->update_action_hold_request($hold) or return $e->event;
816         $e->commit;
817
818         $U->storagereq(
819                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
820
821         return undef;
822 }
823
824
825 __PACKAGE__->register_method(
826         method => 'fetch_open_title_holds',
827         api_name        => 'open-ils.circ.open_holds.retrieve',
828         signature       => q/
829                 Returns a list ids of un-fulfilled holds for a given title id
830                 @param authtoken The login session key
831                 @param id the id of the item whose holds we want to retrieve
832                 @param type The hold type - M, T, V, C
833         /
834 );
835
836 sub fetch_open_title_holds {
837         my( $self, $conn, $auth, $id, $type, $org ) = @_;
838         my $e = new_editor( authtoken => $auth );
839         return $e->event unless $e->checkauth;
840
841         $type ||= "T";
842         $org ||= $e->requestor->ws_ou;
843
844 #       return $e->search_action_hold_request(
845 #               { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
846
847         # XXX make me return IDs in the future ^--
848         my $holds = $e->search_action_hold_request(
849                 { 
850                         target                          => $id, 
851                         cancel_time                     => undef, 
852                         hold_type                       => $type, 
853                         fulfillment_time        => undef 
854                 }
855         );
856
857         flesh_hold_transits($holds);
858         return $holds;
859 }
860
861
862 sub flesh_hold_transits {
863         my $holds = shift;
864         for my $hold ( @$holds ) {
865                 $hold->transit(
866                         $apputils->simplereq(
867                                 'open-ils.cstore',
868                                 "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
869                                 { hold => $hold->id },
870                                 { order_by => { ahtc => 'id desc' }, limit => 1 }
871                         )->[0]
872                 );
873         }
874 }
875
876 sub flesh_hold_notices {
877         my( $holds, $e ) = @_;
878         $e ||= new_editor();
879
880         for my $hold (@$holds) {
881                 my $notices = $e->search_action_hold_notification(
882                         [
883                                 { hold => $hold->id },
884                                 { order_by => { anh => 'notify_time desc' } },
885                         ],
886                         {idlist=>1}
887                 );
888
889                 $hold->notify_count(scalar(@$notices));
890                 if( @$notices ) {
891                         my $n = $e->retrieve_action_hold_notification($$notices[0])
892                                 or return $e->event;
893                         $hold->notify_time($n->notify_time);
894                 }
895         }
896 }
897
898
899
900
901 __PACKAGE__->register_method(
902         method => 'fetch_captured_holds',
903         api_name        => 'open-ils.circ.captured_holds.on_shelf.retrieve',
904         signature       => q/
905                 Returns a list of un-fulfilled holds for a given title id
906                 @param authtoken The login session key
907                 @param org The org id of the location in question
908         /
909 );
910
911 __PACKAGE__->register_method(
912         method => 'fetch_captured_holds',
913         api_name        => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
914         signature       => q/
915                 Returns a list ids of un-fulfilled holds for a given title id
916                 @param authtoken The login session key
917                 @param org The org id of the location in question
918         /
919 );
920
921 sub fetch_captured_holds {
922         my( $self, $conn, $auth, $org ) = @_;
923
924         my $e = new_editor(authtoken => $auth);
925         return $e->event unless $e->checkauth;
926         return $e->event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
927
928         $org ||= $e->requestor->ws_ou;
929
930         my $holds = $e->search_action_hold_request(
931                 { 
932                         capture_time            => { "!=" => undef },
933                         current_copy            => { "!=" => undef },
934                         fulfillment_time        => undef,
935                         pickup_lib                      => $org,
936                         cancel_time                     => undef,
937                 }
938         );
939
940         my @res;
941         for my $h (@$holds) {
942                 my $copy = $e->retrieve_asset_copy($h->current_copy)
943                         or return $e->event;
944                 push( @res, $h ) if 
945                         $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
946         }
947
948         if( ! $self->api_name =~ /id_list/ ) {
949                 flesh_hold_transits(\@res);
950                 flesh_hold_notices(\@res, $e);
951         }
952
953         if( $self->api_name =~ /id_list/ ) {
954                 return [ map { $_->id } @res ];
955         } else {
956                 return \@res;
957         }
958 }
959
960
961 __PACKAGE__->register_method(
962         method  => "check_title_hold",
963         api_name        => "open-ils.circ.title_hold.is_possible",
964         notes           => q/
965                 Determines if a hold were to be placed by a given user,
966                 whether or not said hold would have any potential copies
967                 to fulfill it.
968                 @param authtoken The login session key
969                 @param params A hash of named params including:
970                         patronid  - the id of the hold recipient
971                         titleid (brn) - the id of the title to be held
972                         depth   - the hold range depth (defaults to 0)
973         /);
974
975 sub check_title_hold {
976         my( $self, $client, $authtoken, $params ) = @_;
977
978         my %params              = %$params;
979         my $titleid             = $params{titleid} ||"";
980         my $volid               = $params{volume_id};
981         my $copyid              = $params{copy_id};
982         my $mrid                        = $params{mrid} ||"";
983         my $depth               = $params{depth} || 0;
984         my $pickup_lib  = $params{pickup_lib};
985         my $hold_type   = $params{hold_type} || 'T';
986
987         my $e = new_editor(authtoken=>$authtoken);
988         return $e->event unless $e->checkauth;
989         my $patron = $e->retrieve_actor_user($params{patronid})
990                 or return $e->event;
991
992         if( $e->requestor->id ne $patron->id ) {
993                 return $e->event unless 
994                         $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
995         }
996
997         return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
998
999         my $rangelib    = $params{range_lib} || $patron->home_ou;
1000
1001         my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
1002                 or return $e->event;
1003
1004         $logger->info("checking hold possibility with type $hold_type");
1005
1006         my $copy;
1007         my $volume;
1008         my $title;
1009
1010         if( $hold_type eq OILS_HOLD_TYPE_COPY ) {
1011
1012                 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
1013                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1014                         or return $e->event;
1015                 $title = $e->retrieve_biblio_record_entry($volume->record)
1016                         or return $e->event;
1017                 return verify_copy_for_hold( 
1018                         $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib );
1019
1020         } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1021
1022                 $volume = $e->retrieve_asset_call_number($volid)
1023                         or return $e->event;
1024                 $title = $e->retrieve_biblio_record_entry($volume->record)
1025                         or return $e->event;
1026
1027                 return _check_volume_hold_is_possible(
1028                         $volume, $title, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1029
1030         } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
1031
1032                 return _check_title_hold_is_possible(
1033                         $titleid, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib);
1034
1035         } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1036
1037                 my $maps = $e->search_metabib_source_map({metarecord=>$mrid});
1038                 my @recs = map { $_->source } @$maps;
1039                 for my $rec (@recs) {
1040                         return 1 if (_check_title_hold_is_possible(
1041                                 $rec, $rangelib, $depth, $request_lib, $patron, $e->requestor, $pickup_lib));
1042                 }
1043                 return 0;       
1044         }
1045 }
1046
1047
1048
1049 sub _check_title_hold_is_possible {
1050         my( $titleid, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1051
1052         my $limit       = 10;
1053         my $offset      = 0;
1054         my $title;
1055
1056         $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
1057
1058         while( $title = $U->storagereq(
1059                                 'open-ils.storage.biblio.record_entry.ranged_tree', 
1060                                 $titleid, $rangelib, $depth, $limit, $offset ) ) {
1061
1062                 last unless 
1063                         ref($title) and 
1064                         ref($title->call_numbers) and 
1065                         @{$title->call_numbers};
1066
1067                 for my $cn (@{$title->call_numbers}) {
1068         
1069                         $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
1070         
1071                         for my $copy (@{$cn->copies}) {
1072                                 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
1073                                 return 1 if verify_copy_for_hold( 
1074                                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1075                                 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
1076                         }
1077                 }
1078
1079                 $offset += $limit;
1080         }
1081         return 0;
1082 }
1083
1084 sub _check_volume_hold_is_possible {
1085         my( $vol, $title, $rangelib, $depth, $request_lib, $patron, $requestor, $pickup_lib ) = @_;
1086         my $copies = new_editor->search_asset_copy({call_number => $vol->id});
1087         $logger->info("checking possibility of volume hold for volume ".$vol->id);
1088         for my $copy ( @$copies ) {
1089                 return 1 if verify_copy_for_hold( 
1090                         $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
1091         }
1092         return 0;
1093 }
1094
1095
1096
1097 sub verify_copy_for_hold {
1098         my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
1099         $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
1100         return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
1101                 {       patron                          => $patron, 
1102                         requestor                       => $requestor, 
1103                         copy                                    => $copy,
1104                         title                                   => $title, 
1105                         title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
1106                         pickup_lib                      => $pickup_lib,
1107                         request_lib                     => $request_lib 
1108                 } 
1109         );
1110         return 0;
1111 }
1112
1113
1114
1115 sub find_nearest_permitted_hold {
1116
1117         my $class       = shift;
1118         my $editor      = shift; # CStoreEditor object
1119         my $copy                = shift; # copy to target
1120         my $user                = shift; # hold recipient
1121         my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
1122         my $evt         = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
1123
1124         my $bc = $copy->barcode;
1125
1126         # find any existing holds that already target this copy
1127         my $old_holds = $editor->search_action_hold_request(
1128                 {       current_copy => $copy->id, 
1129                         cancel_time => undef, 
1130                         capture_time => undef 
1131                 } 
1132         );
1133
1134         # hold->type "R" means we need this copy
1135         for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
1136
1137         $logger->info("circulator: searching for best hold at org ".$user->ws_ou." and copy $bc");
1138
1139         # search for what should be the best holds for this copy to fulfill
1140         my $best_holds = $U->storagereq(
1141                 "open-ils.storage.action.hold_request.nearest_hold.atomic",
1142                 $user->ws_ou, $copy->id, 10 );
1143
1144         unless(@$best_holds) {
1145
1146                 if( my $hold = $$old_holds[0] ) {
1147                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1148                         return ($hold);
1149                 }
1150
1151                 $logger->info("circulator: no suitable holds found for copy $bc");
1152                 return (undef, $evt);
1153         }
1154
1155
1156         my $best_hold;
1157
1158         # for each potential hold, we have to run the permit script
1159         # to make sure the hold is actually permitted.
1160         for my $holdid (@$best_holds) {
1161                 next unless $holdid;
1162                 $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
1163
1164                 my $hold = $editor->retrieve_action_hold_request($holdid) or next;
1165                 my $reqr = $editor->retrieve_actor_user($hold->requestor) or next;
1166                 my $rlib = $editor->retrieve_actor_org_unit($hold->request_lib) or next;
1167
1168                 # see if this hold is permitted
1169                 my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
1170                         {       patron_id                       => $hold->usr,
1171                                 requestor                       => $reqr->id,
1172                                 copy                                    => $copy,
1173                                 pickup_lib                      => $hold->pickup_lib,
1174                                 request_lib                     => $rlib,
1175                         } 
1176                 );
1177
1178                 if( $permitted ) {
1179                         $best_hold = $hold;
1180                         last;
1181                 }
1182         }
1183
1184
1185         unless( $best_hold ) { # no "good" permitted holds were found
1186                 if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
1187                         $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
1188                         return ($hold);
1189                 }
1190
1191                 # we got nuthin
1192                 $logger->info("circulator: no suitable holds found for copy $bc");
1193                 return (undef, $evt);
1194         }
1195
1196         $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
1197
1198         # indicate a permitted hold was found
1199         return $best_hold if $check_only;
1200
1201         # we've found a permitted hold.  we need to "grab" the copy 
1202         # to prevent re-targeted holds (next part) from re-grabbing the copy
1203         $best_hold->current_copy($copy->id);
1204         $editor->update_action_hold_request($best_hold) 
1205                 or return (undef, $editor->event);
1206
1207
1208         # re-target any other holds that already target this copy
1209         for my $old_hold (@$old_holds) {
1210                 next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
1211                 $logger->info("circulator: re-targeting hold ".$old_hold->id.
1212                         " after a better hold [".$best_hold->id."] was found");
1213                 $U->storagereq( 
1214                         'open-ils.storage.action.hold_request.copy_targeter', undef, $old_hold->id );
1215         }
1216
1217         return ($best_hold);
1218 }
1219
1220
1221
1222
1223
1224
1225 __PACKAGE__->register_method(
1226         method => 'all_rec_holds',
1227         api_name => 'open-ils.circ.holds.retrieve_all_from_title',
1228 );
1229
1230 sub all_rec_holds {
1231         my( $self, $conn, $auth, $title_id, $args ) = @_;
1232
1233         my $e = new_editor(authtoken=>$auth);
1234         $e->checkauth or return $e->event;
1235         $e->allowed('VIEW_HOLD') or return $e->event;
1236
1237         $args ||= { fulfillment_time => undef };
1238         $args->{cancel_time} = undef;
1239
1240         my $resp = { volume_holds => [], copy_holds => [] };
1241
1242         $resp->{title_holds} = $e->search_action_hold_request(
1243                 { 
1244                         hold_type => OILS_HOLD_TYPE_TITLE, 
1245                         target => $title_id, 
1246                         %$args 
1247                 }, {idlist=>1} );
1248
1249         my $vols = $e->search_asset_call_number(
1250                 { record => $title_id, deleted => 'f' }, {idlist=>1});
1251
1252         return $resp unless @$vols;
1253
1254         $resp->{volume_holds} = $e->search_action_hold_request(
1255                 { 
1256                         hold_type => OILS_HOLD_TYPE_VOLUME, 
1257                         target => $vols,
1258                         %$args }, 
1259                 {idlist=>1} );
1260
1261         my $copies = $e->search_asset_copy(
1262                 { call_number => $vols, deleted => 'f' }, {idlist=>1});
1263
1264         return $resp unless @$copies;
1265
1266         $resp->{copy_holds} = $e->search_action_hold_request(
1267                 { 
1268                         hold_type => OILS_HOLD_TYPE_COPY,
1269                         target => $copies,
1270                         %$args }, 
1271                 {idlist=>1} );
1272
1273         return $resp;
1274 }
1275
1276
1277
1278
1279
1280 __PACKAGE__->register_method(
1281         method => 'uber_hold',
1282         api_name => 'open-ils.circ.hold.details.retrieve'
1283 );
1284
1285 sub uber_hold {
1286         my($self, $client, $auth, $hold_id) = @_;
1287         my $e = new_editor(authtoken=>$auth);
1288         $e->checkauth or return $e->event;
1289         $e->allowed('VIEW_HOLD') or return $e->event;
1290
1291         my $resp = {};
1292
1293         my $hold = $e->retrieve_action_hold_request(
1294                 [
1295                         $hold_id,
1296                         {
1297                                 flesh => 1,
1298                                 flesh_fields => { ahr => [ 'current_copy', 'usr' ] }
1299                         }
1300                 ]
1301         ) or return $e->event;
1302
1303         my $user = $hold->usr;
1304         $hold->usr($user->id);
1305
1306         my $card = $e->retrieve_actor_card($user->card)
1307                 or return $e->event;
1308
1309         my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
1310
1311         flesh_hold_notices([$hold], $e);
1312         flesh_hold_transits([$hold]);
1313
1314         return {
1315                 hold            => $hold,
1316                 copy            => $copy,
1317                 volume  => $volume,
1318                 mvr             => $mvr,
1319                 status  => _hold_status($e, $hold),
1320                 patron_first => $user->first_given_name,
1321                 patron_last  => $user->family_name,
1322                 patron_barcode => $card->barcode,
1323         };
1324 }
1325
1326
1327
1328 # -----------------------------------------------------
1329 # Returns the MVR object that represents what the
1330 # hold is all about
1331 # -----------------------------------------------------
1332 sub find_hold_mvr {
1333         my( $e, $hold ) = @_;
1334
1335         my $tid;
1336         my $copy;
1337         my $volume;
1338
1339         if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
1340                 my $mr = $e->retrieve_metabib_metarecord($hold->target)
1341                         or return $e->event;
1342                 $tid = $mr->master_record;
1343
1344         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
1345                 $tid = $hold->target;
1346
1347         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
1348                 $volume = $e->retrieve_asset_call_number($hold->target)
1349                         or return $e->event;
1350                 $tid = $volume->record;
1351
1352         } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
1353                 $copy = $e->retrieve_asset_copy($hold->target)
1354                         or return $e->event;
1355                 $volume = $e->retrieve_asset_call_number($copy->call_number)
1356                         or return $e->event;
1357                 $tid = $volume->record;
1358         }
1359
1360         if(!$copy and ref $hold->current_copy ) {
1361                 $copy = $hold->current_copy;
1362                 $hold->current_copy($copy->id);
1363         }
1364
1365         if(!$volume and $copy) {
1366                 $volume = $e->retrieve_asset_call_number($copy->call_number);
1367         }
1368
1369         my $title = $e->retrieve_biblio_record_entry($tid);
1370         return ( $U->record_to_mvr($title), $volume, $copy );
1371 }
1372
1373
1374
1375
1376 1;