]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
typo
[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 OpenILS::EX;
23 use OpenSRF::EX qw(:try);
24 use OpenILS::Perm;
25 use OpenILS::Event;
26 use OpenSRF::Utils::Logger qw(:logger);
27
28 my $apputils = "OpenILS::Application::AppUtils";
29 my $U = $apputils;
30
31
32
33 __PACKAGE__->register_method(
34         method  => "create_hold",
35         api_name        => "open-ils.circ.holds.create",
36         notes           => <<NOTE);
37 Create a new hold for an item.  From a permissions perspective, 
38 the login session is used as the 'requestor' of the hold.  
39 The hold recipient is determined by the 'usr' setting within
40 the hold object.
41
42 First we verify the requestion has holds request permissions.
43 Then we verify that the recipient is allowed to make the given hold.
44 If not, we see if the requestor has "override" capabilities.  If not,
45 a permission exception is returned.  If permissions allow, we cycle
46 through the set of holds objects and create.
47
48 If the recipient does not have permission to place multiple holds
49 on a single title and said operation is attempted, a permission
50 exception is returned
51 NOTE
52
53 sub create_hold {
54         my( $self, $client, $login_session, @holds) = @_;
55
56         if(!@holds){return 0;}
57         my( $user, $evt ) = $apputils->checkses($login_session);
58         return $evt if $evt;
59
60         my $holds;
61         if(ref($holds[0]) eq 'ARRAY') {
62                 $holds = $holds[0];
63         } else { $holds = [ @holds ]; }
64
65         $logger->debug("Iterating over holds requests...");
66
67         for my $hold (@$holds) {
68
69                 if(!$hold){next};
70                 my $type = $hold->hold_type;
71
72                 $logger->activity("User " . $user->id . 
73                         " creating new hold of type $type for user " . $hold->usr);
74
75                 my $recipient;
76                 if($user->id ne $hold->usr) {
77                         ( $recipient, $evt ) = $apputils->fetch_user($hold->usr);
78                         return $evt if $evt;
79
80                 } else {
81                         $recipient = $user;
82                 }
83
84
85                 my $perm = undef;
86
87                 # am I allowed to place holds for this user?
88                 if($hold->requestor ne $hold->usr) {
89                         $perm = _check_request_holds_perm($user->id, $user->home_ou);
90                         if($perm) { return $perm; }
91                 }
92
93                 # is this user allowed to have holds of this type?
94                 $perm = _check_holds_perm($type, $hold->usr, $recipient->home_ou);
95                 if($perm) { 
96                         #if there is a requestor, see if the requestor has override privelages
97                         if($hold->requestor ne $hold->usr) {
98                                 $perm = _check_request_holds_override($user->id, $user->home_ou);
99                                 if($perm) {return $perm;}
100
101                         } else {
102                                 return $perm; 
103                         }
104                 }
105
106                 #enforce the fact that the login is the one requesting the hold
107                 $hold->requestor($user->id); 
108
109                 my $resp = $apputils->simplereq(
110                         'open-ils.storage',
111                         'open-ils.storage.direct.action.hold_request.create', $hold );
112
113                 if(!$resp) { 
114                         return OpenSRF::EX::ERROR ("Error creating hold"); 
115                 }
116         }
117
118         return 1;
119 }
120
121 # makes sure that a user has permission to place the type of requested hold
122 # returns the Perm exception if not allowed, returns undef if all is well
123 sub _check_holds_perm {
124         my($type, $user_id, $org_id) = @_;
125
126         my $evt;
127         if($type eq "M") {
128                 if($evt = $apputils->check_perms(
129                         $user_id, $org_id, "MR_HOLDS")) {
130                         return $evt;
131                 } 
132
133         } elsif ($type eq "T") {
134                 if($evt = $apputils->check_perms(
135                         $user_id, $org_id, "TITLE_HOLDS")) {
136                         return $evt;
137                 }
138
139         } elsif($type eq "V") {
140                 if($evt = $apputils->check_perms(
141                         $user_id, $org_id, "VOLUME_HOLDS")) {
142                         return $evt;
143                 }
144
145         } elsif($type eq "C") {
146                 if($evt = $apputils->check_perms(
147                         $user_id, $org_id, "COPY_HOLDS")) {
148                         return $evt;
149                 }
150         }
151
152         return undef;
153 }
154
155 # tests if the given user is allowed to place holds on another's behalf
156 sub _check_request_holds_perm {
157         my $user_id = shift;
158         my $org_id = shift;
159         if(my $evt = $apputils->check_perms(
160                 $user_id, $org_id, "REQUEST_HOLDS")) {
161                 return $evt;
162         }
163 }
164
165 sub _check_request_holds_override {
166         my $user_id = shift;
167         my $org_id = shift;
168         if(my $evt = $apputils->check_perms(
169                 $user_id, $org_id, "REQUEST_HOLDS_OVERRIDE")) {
170                 return $evt;
171         }
172 }
173
174
175 __PACKAGE__->register_method(
176         method  => "retrieve_holds",
177         api_name        => "open-ils.circ.holds.retrieve",
178         notes           => <<NOTE);
179 Retrieves all the holds, with hold transits attached, for the specified
180 user id.  The login session is the requestor and if the requestor is
181 different from the user, then the requestor must have VIEW_HOLD permissions.
182 NOTE
183
184
185 sub retrieve_holds {
186         my($self, $client, $login_session, $user_id) = @_;
187
188         my( $user, $target, $evt ) = $apputils->checkses_requestor(
189                 $login_session, $user_id, 'VIEW_HOLD' );
190         return $evt if $evt;
191
192         my $holds = $apputils->simplereq(
193                 'open-ils.storage',
194                 "open-ils.storage.direct.action.hold_request.search.atomic",
195                 "usr" =>  $user_id , fulfillment_time => undef, { order_by => "request_time" });
196         
197         for my $hold ( @$holds ) {
198                 $hold->transit(
199                         $apputils->simplereq(
200                                 'open-ils.storage',
201                                 "open-ils.storage.direct.action.hold_transit_copy.search.hold.atomic" => $hold->id,
202                                 { order_by => 'id desc', limit => 1 }
203                         )->[0]
204                 );
205         }
206
207         return $holds;
208 }
209
210 __PACKAGE__->register_method(
211         method  => "retrieve_holds_by_pickup_lib",
212         api_name        => "open-ils.circ.holds.retrieve_by_pickup_lib",
213         notes           => <<NOTE);
214 Retrieves all the holds, with hold transits attached, for the specified
215 pickup_ou id. 
216 NOTE
217
218
219 sub retrieve_holds_by_pickup_lib {
220         my($self, $client, $login_session, $ou_id) = @_;
221
222         #FIXME -- put an appropriate permission check here
223         #my( $user, $target, $evt ) = $apputils->checkses_requestor(
224         #       $login_session, $user_id, 'VIEW_HOLD' );
225         #return $evt if $evt;
226
227         my $holds = $apputils->simplereq(
228                 'open-ils.storage',
229                 "open-ils.storage.direct.action.hold_request.search.atomic",
230                 "pickup_lib" =>  $ou_id , fulfillment_time => undef, { order_by => "request_time" });
231         
232         for my $hold ( @$holds ) {
233                 $hold->transit(
234                         $apputils->simplereq(
235                                 'open-ils.storage',
236                                 "open-ils.storage.direct.action.hold_transit_copy.search.hold.atomic" => $hold->id,
237                                 { order_by => 'id desc', limit => 1 }
238                         )->[0]
239                 );
240         }
241
242         return $holds;
243 }
244
245
246 __PACKAGE__->register_method(
247         method  => "cancel_hold",
248         api_name        => "open-ils.circ.hold.cancel",
249         notes           => <<"  NOTE");
250         Cancels the specified hold.  The login session
251         is the requestor and if the requestor is different from the usr field
252         on the hold, the requestor must have CANCEL_HOLDS permissions.
253         the hold may be either the hold object or the hold id
254         NOTE
255
256 sub cancel_hold {
257         my($self, $client, $login_session, $holdid) = @_;
258         my $hold;       
259
260         my($user, $evt) = $U->checkses($login_session);
261         return $evt if $evt;
262
263         ( $hold, $evt ) = $U->fetch_hold($holdid);
264         return $evt if $evt;
265
266         if($user->id ne $hold->usr) { #am I allowed to cancel this user's hold?
267                 if($evt = $apputils->check_perms(
268                         $user->id, $user->home_ou, 'CANCEL_HOLDS')) {
269                         return $evt;
270                 }
271         }
272
273         $logger->activity( "User " . $user->id . 
274                 " canceling hold $holdid for user " . $hold->usr );
275
276         return $apputils->simplereq(
277                 'open-ils.storage',
278                 "open-ils.storage.direct.action.hold_request.delete", $hold );
279 }
280
281
282 __PACKAGE__->register_method(
283         method  => "update_hold",
284         api_name        => "open-ils.circ.hold.update",
285         notes           => <<"  NOTE");
286         Updates the specified hold.  The login session
287         is the requestor and if the requestor is different from the usr field
288         on the hold, the requestor must have UPDATE_HOLDS permissions.
289         NOTE
290
291 sub update_hold {
292         my($self, $client, $login_session, $hold) = @_;
293
294         my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
295                 $login_session, $hold->usr, 'UPDATE_HOLD' );
296         return $evt if $evt;
297
298         $logger->activity('User ' . $requestor->id . 
299                 ' updating hold ' . $hold->id . ' for user ' . $target->id );
300
301         return $U->storagereq(
302                 "open-ils.storage.direct.action.hold_request.update", $hold );
303 }
304
305
306 __PACKAGE__->register_method(
307         method  => "retrieve_hold_status",
308         api_name        => "open-ils.circ.hold.status.retrieve",
309         notes           => <<"  NOTE");
310         Calculates the current status of the hold.
311         the requestor must have VIEW_HOLD permissions if the hold is for a user
312         other than the requestor.
313         Returns -1  on error (for now)
314         Returns 1 for 'waiting for copy to become available'
315         Returns 2 for 'waiting for copy capture'
316         Returns 3 for 'in transit'
317         Returns 4 for 'arrived'
318         NOTE
319
320 sub retrieve_hold_status {
321         my($self, $client, $login_session, $hold_id) = @_;
322
323
324         my( $requestor, $target, $hold, $copy, $transit, $evt );
325
326         ( $hold, $evt ) = $apputils->fetch_hold($hold_id);
327         return $evt if $evt;
328
329         ( $requestor, $target, $evt ) = $apputils->checkses_requestor(
330                 $login_session, $hold->usr, 'VIEW_HOLD' );
331         return $evt if $evt;
332
333         return 1 unless (defined($hold->current_copy));
334         
335         ( $copy, $evt ) = $apputils->fetch_copy($hold->current_copy);
336         return $evt if $evt;
337
338         return 4 if ($hold->capture_time and $copy->circ_lib eq $hold->pickup_lib);
339
340         ( $transit, $evt ) = $apputils->fetch_hold_transit_by_hold( $hold->id );
341         return 4 if(ref($transit) and defined($transit->dest_recv_time) ); 
342
343         return 3 if defined($hold->capture_time);
344
345         return 2;
346 }
347
348
349
350
351
352 __PACKAGE__->register_method(
353         method  => "capture_copy",
354         api_name        => "open-ils.circ.hold.capture_copy.barcode",
355         notes           => <<"  NOTE");
356         Captures a copy to fulfil a hold
357         Params is login session and copy barcode
358         Optional param is 'flesh'.  If set, we also return the
359         relevant copy and title
360         login mus have COPY_CHECKIN permissions (since this is essentially
361         copy checkin)
362         NOTE
363
364 # XXX deprecate me XXX
365
366 sub capture_copy {
367         my( $self, $client, $login_session, $params ) = @_;
368         my %params = %$params;
369         my $barcode = $params{barcode};
370
371
372         my( $user, $target, $copy, $hold, $evt );
373
374         ( $user, $evt ) = $apputils->checkses($login_session);
375         return $evt if $evt;
376
377         # am I allowed to checkin a copy?
378         $evt = $apputils->check_perms($user->id, $user->home_ou, "COPY_CHECKIN");
379         return $evt if $evt;
380
381         $logger->info("Capturing copy with barcode $barcode");
382
383         my $session = $apputils->start_db_session();
384
385         ($copy, $evt) = $apputils->fetch_copy_by_barcode($barcode);
386         return $evt if $evt;
387
388         $logger->debug("Capturing copy " . $copy->id);
389
390         ( $hold, $evt ) = _find_local_hold_for_copy($session, $copy, $user);
391         return $evt if $evt;
392
393         warn "Found hold " . $hold->id . "\n";
394         $logger->info("We found a hold " .$hold->id. "for capturing copy with barcode $barcode");
395
396         $hold->current_copy($copy->id);
397         $hold->capture_time("now"); 
398
399         #update the hold
400         my $stat = $session->request(
401                         "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
402         if(!$stat) { throw OpenSRF::EX::ERROR 
403                 ("Error updating hold request " . $copy->id); }
404
405         $copy->status(8); #status on holds shelf
406
407         # if the staff member capturing this item is not at the pickup lib
408         if( $user->home_ou ne $hold->pickup_lib ) {
409                 $self->_build_hold_transit( $login_session, $session, $hold, $user, $copy );
410         }
411
412         $copy->editor($user->id);
413         $copy->edit_date("now");
414         $stat = $session->request(
415                 "open-ils.storage.direct.asset.copy.update", $copy )->gather(1);
416         if(!$stat) { throw OpenSRF::EX ("Error updating copy " . $copy->id); }
417
418         my $payload = { hold => $hold };
419         $payload->{copy} = $copy if $params{flesh_copy};
420
421         if($params{flesh_record}) {
422                 my $record;
423                 ($record, $evt) = $apputils->fetch_record_by_copy( $copy->id );
424                 return $evt if $evt;
425                 $record = $apputils->record_to_mvr($record);
426                 $payload->{record} = $record;
427         }
428
429         $apputils->commit_db_session($session);
430
431         return OpenILS::Event->new('ROUTE_ITEM', 
432                 route_to => $hold->pickup_lib, payload => $payload );
433 }
434
435 sub _build_hold_transit {
436         my( $self, $login_session, $session, $hold, $user, $copy ) = @_;
437         my $trans = Fieldmapper::action::hold_transit_copy->new;
438
439         $trans->hold($hold->id);
440         $trans->source($user->home_ou);
441         $trans->dest($hold->pickup_lib);
442         $trans->source_send_time("now");
443         $trans->target_copy($copy->id);
444         $trans->copy_status($copy->status);
445
446         my $meth = $self->method_lookup("open-ils.circ.hold_transit.create");
447         my ($stat) = $meth->run( $login_session, $trans, $session );
448         if(!$stat) { throw OpenSRF::EX ("Error creating new hold transit"); }
449         else { $copy->status(6); } #status in transit 
450 }
451
452
453 sub find_local_hold {
454         my( $class, $session, $copy, $user ) = @_;
455         return _find_local_hold_for_copy($session, $copy, $user);
456 }
457
458 sub _find_local_hold_for_copy {
459
460         my $session = shift;
461         my $copy = shift;
462         my $user = shift;
463         my $evt = OpenILS::Event->new('HOLD_NOT_FOUND');
464
465         # first see if this copy has already been selected to fulfill a hold
466         my $hold  = $session->request(
467                 "open-ils.storage.direct.action.hold_request.search_where",
468                 { current_copy => $copy->id, capture_time => undef } )->gather(1);
469
470         if($hold) {return $hold;}
471
472         $logger->debug("searching for local hold at org " . 
473                 $user->home_ou . " and copy " . $copy->id);
474
475         my $holdid = $session->request(
476                 "open-ils.storage.action.hold_request.nearest_hold",
477                 $user->home_ou, $copy->id )->gather(1);
478
479         return (undef, $evt) unless defined $holdid;
480
481         $logger->debug("Found hold id $holdid while ".
482                 "searching nearest hold to " .$user->home_ou);
483
484         return $apputils->fetch_hold($holdid);
485 }
486
487
488 __PACKAGE__->register_method(
489         method  => "create_hold_transit",
490         api_name        => "open-ils.circ.hold_transit.create",
491         notes           => <<"  NOTE");
492         Creates a new transit object
493         NOTE
494
495 sub create_hold_transit {
496         my( $self, $client, $login_session, $transit, $session ) = @_;
497
498         my( $user, $evt ) = $apputils->checkses($login_session);
499         return $evt if $evt;
500         $evt = $apputils->check_perms($user->id, $user->home_ou, "CREATE_TRANSIT");
501         return $evt if $evt;
502
503         my $ses;
504         if($session) { $ses = $session; } 
505         else { $ses = OpenSRF::AppSession->create("open-ils.storage"); }
506
507         return $ses->request(
508                 "open-ils.storage.direct.action.hold_transit_copy.create", $transit )->gather(1);
509 }
510
511
512 sub fetch_open_hold_by_current_copy {
513         my $class = shift;
514         my $copyid = shift;
515         my $hold = $apputils->simplereq(
516                 'open-ils.storage', 
517                 'open-ils.storage.direct.action.hold_request.search.atomic',
518                          current_copy =>  $copyid , fulfillment_time => undef );
519         return $hold->[0] if ref($hold);
520         return undef;
521 }
522
523 sub fetch_related_holds {
524         my $class = shift;
525         my $copyid = shift;
526         return $apputils->simplereq(
527                 'open-ils.storage', 
528                 'open-ils.storage.direct.action.hold_request.search.atomic',
529                          current_copy =>  $copyid , fulfillment_time => undef );
530 }
531
532
533 __PACKAGE__->register_method (
534         method          => "hold_pull_list",
535         api_name                => "open-ils.circ.hold_pull_list.retrieve",
536         signature       => q/
537                 Returns a list of hold ID's that need to be "pulled"
538                 by a given location
539         /
540 );
541
542 sub hold_pull_list {
543         my( $self, $conn, $authtoken, $limit, $offset ) = @_;
544         my( $reqr, $evt ) = $U->checkses($authtoken);
545         return $evt if $evt;
546
547         my $org = $reqr->ws_ou || $reqr->home_ou;
548         # the perm locaiton shouldn't really matter here since holds
549         # will exist all over and VIEW_HOLDS should be universal
550         $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
551         return $evt if $evt;
552
553         return $U->storagereq(
554                 'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.atomic',
555                 $org, $limit, $offset ); # XXX change to workstation
556 }
557
558 __PACKAGE__->register_method (
559         method          => 'fetch_hold_notify',
560         api_name                => 'open-ils.circ.hold_notification.retrieve_by_hold',
561         signature       => q/ 
562                 Returns a list of hold notification objects based on hold id.
563                 @param authtoken The loggin session key
564                 @param holdid The id of the hold whose notifications we want to retrieve
565                 @return An array of hold notification objects, event on error.
566         /
567 );
568
569 sub fetch_hold_notify {
570         my( $self, $conn, $authtoken, $holdid ) = @_;
571         my( $requestor, $evt ) = $U->checkses($authtoken);
572         return $evt if $evt;
573         my ($hold, $patron);
574         ($hold, $evt) = $U->fetch_hold($holdid);
575         return $evt if $evt;
576         ($patron, $evt) = $U->fetch_user($hold->usr);
577         return $evt if $evt;
578
579         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
580         return $evt if $evt;
581
582         $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
583         return $U->storagereq(
584                 'open-ils.storage.direct.action.hold_notification.search.hold.atomic', $holdid );
585 }
586
587
588 __PACKAGE__->register_method (
589         method          => 'create_hold_notify',
590         api_name                => 'open-ils.circ.hold_notification.create',
591         signature       => q/
592                 Creates a new hold notification object
593                 @param authtoken The login session key
594                 @param notification The hold notification object to create
595                 @return ID of the new object on success, Event on error
596                 /
597 );
598 sub create_hold_notify {
599         my( $self, $conn, $authtoken, $notification ) = @_;
600         my( $requestor, $evt ) = $U->checkses($authtoken);
601         return $evt if $evt;
602         my ($hold, $patron);
603         ($hold, $evt) = $U->fetch_hold($notification->hold);
604         return $evt if $evt;
605         ($patron, $evt) = $U->fetch_user($hold->usr);
606         return $evt if $evt;
607
608         # XXX perm depth probably doesn't matter here -- should always be consortium level
609         $evt = $U->check_perms($requestor->id, $patron->home_ou, 'CREATE_HOLD_NOTIFICATION');
610         return $evt if $evt;
611
612         # Set the proper notifier 
613         $notification->notify_staff($requestor->id);
614         my $id = $U->storagereq(
615                 'open-ils.storage.direct.action.hold_notification.create', $notification );
616         return $U->DB_UPDATE_FAILED($notification) unless $id;
617         $logger->info("User ".$requestor->id." successfully created new hold notification $id");
618         return $id;
619 }
620
621
622 __PACKAGE__->register_method(
623         method  => 'reset_hold',
624         api_name        => 'open-ils.circ.hold.reset',
625         signature       => q/
626                 Un-captures and un-targets a hold, essentially returning
627                 it to the state it was in directly after it was placed,
628                 then attempts to re-target the hold
629                 @param authtoken The login session key
630                 @param holdid The id of the hold
631         /
632 );
633
634
635 sub reset_hold {
636         my( $self, $conn, $auth, $holdid ) = @_;
637         my $reqr;
638         my ($hold, $evt) = $U->fetch_hold($holdid);
639         return $evt if $evt;
640         ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
641         return $evt if $evt;
642         $evt = $self->_reset_hold($reqr, $hold);
643         return $evt if $evt;
644         return 1;
645 }
646
647 sub _reset_hold {
648         my ($self, $reqr, $hold, $session) = @_;
649
650         my $x;
651         if(!$session) {
652                 $x = 1;
653                 $session = $U->start_db_session();
654         }
655
656         $hold->clear_capture_time;
657         $hold->clear_current_copy;
658
659         return $U->DB_UPDATE_FAILED($hold) unless 
660                 $session->request(
661                         'open-ils.storage.direct.action.hold_request.update', $hold )->gather(1);
662
663         $session->request(
664                 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id )->gather(1);
665
666         $U->commit_db_session($session) unless $x;
667         return undef;
668 }
669
670
671
672 1;