]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
More circ work
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Circ / Circulate.pm
1 package OpenILS::Application::Circ::Circulate;
2 use base 'OpenSRF::Application';
3 use strict; use warnings;
4 use OpenSRF::EX qw(:try);
5 use OpenSRF::Utils;
6 use Data::Dumper;
7 use OpenSRF::Utils::Logger qw(:logger);
8 use OpenILS::Utils::ScriptRunner;
9 use OpenILS::Application::AppUtils;
10 use OpenILS::Application::Circ::Holds;
11 $Data::Dumper::Indent = 0;
12 my $apputils = "OpenILS::Application::AppUtils";
13 my $U = $apputils;
14 my $holdcode = "OpenILS::Application::Circ::Holds";
15
16 my %scripts;                    # - circulation script filenames
17 my $script_libs;                # - any additional script libraries
18 my %cache;                              # - db objects cache
19 my %contexts;                   # - Script runner contexts
20
21 # ------------------------------------------------------------------------------
22 # Load the circ script from the config
23 # ------------------------------------------------------------------------------
24 sub initialize {
25
26         my $self = shift;
27         my $conf = OpenSRF::Utils::SettingsClient->new;
28         my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
29         my @pfx = ( @pfx2, "scripts" );
30
31         my $p           = $conf->config_value(  @pfx, 'circ_permit_patron' );
32         my $c           = $conf->config_value(  @pfx, 'circ_permit_copy' );
33         my $d           = $conf->config_value(  @pfx, 'circ_duration' );
34         my $f           = $conf->config_value(  @pfx, 'circ_recurring_fines' );
35         my $m           = $conf->config_value(  @pfx, 'circ_max_fines' );
36         my $pr  = $conf->config_value(  @pfx, 'renew_permit' );
37         my $ph  = $conf->config_value(  @pfx, 'hold_permit' );
38         my $lb  = $conf->config_value(  @pfx2, 'script_path' );
39
40         $logger->error( "Missing circ script(s)" ) 
41                 unless( $p and $c and $d and $f and $m and $pr and $ph );
42
43         $scripts{circ_permit_patron}    = $p;
44         $scripts{circ_permit_copy}              = $c;
45         $scripts{circ_duration}                 = $d;
46         $scripts{circ_recurring_fines}= $f;
47         $scripts{circ_max_fines}                = $m;
48         $scripts{circ_renew_permit}     = $pr;
49         $scripts{hold_permit}                   = $ph;
50
51         $lb = [ $lb ] unless ref($lb);
52         $script_libs = $lb;
53
54         $logger->debug("Loaded rules scripts for circ: " .
55                 "circ permit patron: $p, circ permit copy: $c, ".
56                 "circ duration :$d , circ recurring fines : $f, " .
57                 "circ max fines : $m, circ renew permit : $pr, permit hold: $ph");
58 }
59
60
61 # ------------------------------------------------------------------------------
62 # Loads the necessary circ objects and pushes them into the script environment
63 # Returns ( $data, $evt ).  if $evt is defined, then an
64 # unexpedted event occurred and should be dealt with / returned to the caller
65 # ------------------------------------------------------------------------------
66 sub create_circ_ctx {
67         my %params = @_;
68
69         my $evt;
70         my $ctx = \%params;
71
72         $evt = _ctx_add_patron_objects($ctx, %params);
73         return $evt if $evt;
74
75         if( ($params{copy} or $params{copyid} or $params{barcode}) and !$params{noncat} ) {
76                 $evt = _ctx_add_copy_objects($ctx, %params);
77                 return $evt if $evt;
78         }
79
80         _doctor_patron_object($ctx) if $ctx->{patron};
81         _doctor_copy_object($ctx) if $ctx->{copy};
82         _doctor_circ_objects($ctx);
83         _build_circ_script_runner($ctx);
84         _add_script_runner_methods( $ctx );
85
86         return $ctx;
87 }
88
89 sub _ctx_add_patron_objects {
90         my( $ctx, %params) = @_;
91
92         $ctx->{patron}  = $params{patron};
93
94         if(!defined($cache{patron_standings})) {
95                 $cache{patron_standings} = $apputils->fetch_patron_standings();
96                 $cache{group_tree} = $apputils->fetch_permission_group_tree();
97         }
98
99         $ctx->{patron_standings} = $cache{patron_standings};
100         $ctx->{group_tree} = $cache{group_tree};
101
102         $ctx->{patron_circ_summary} = 
103                 $apputils->fetch_patron_circ_summary($ctx->{patron}->id) 
104                 if $params{fetch_patron_circsummary};
105
106         return undef;
107 }
108
109
110 sub _ctx_add_copy_objects {
111         my($ctx, %params)  = @_;
112         my $evt;
113
114         $cache{copy_statuses} = $apputils->fetch_copy_statuses 
115                 if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
116
117         $cache{copy_locations} = $apputils->fetch_copy_locations 
118                 if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
119
120         $ctx->{copy_statuses} = $cache{copy_statuses};
121         $ctx->{copy_locations} = $cache{copy_locations};
122
123         my $copy = $params{copy} if $params{copy};
124
125         if(!$copy) {
126
127                 ( $copy, $evt ) = 
128                         $apputils->fetch_copy($params{copyid}) if $params{copyid};
129                 return $evt if $evt;
130
131                 if(!$copy) {
132                         ( $copy, $evt ) = 
133                                 $apputils->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
134                         return $evt if $evt;
135                 }
136         }
137
138         $ctx->{copy} = $copy;
139
140         ( $ctx->{title}, $evt ) = $apputils->fetch_record_by_copy( $ctx->{copy}->id );
141         return $evt if $evt;
142
143         return undef;
144 }
145
146
147 # ------------------------------------------------------------------------------
148 # Fleshes parts of the patron object
149 # ------------------------------------------------------------------------------
150 sub _doctor_copy_object {
151
152         my $ctx = shift;
153         my $copy = $ctx->{copy};
154
155         # set the copy status to a status name
156         $copy->status( _get_copy_status( 
157                 $copy, $ctx->{copy_statuses} ) ) if $copy;
158
159         # set the copy location to the location object
160         $copy->location( _get_copy_location( 
161                 $copy, $ctx->{copy_locations} ) ) if $copy;
162
163         $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
164 }
165
166
167 # ------------------------------------------------------------------------------
168 # Fleshes parts of the copy object
169 # ------------------------------------------------------------------------------
170 sub _doctor_patron_object {
171         my $ctx = shift;
172         my $patron = $ctx->{patron};
173
174         # push the standing object into the patron
175         if(ref($ctx->{patron_standings})) {
176                 for my $s (@{$ctx->{patron_standings}}) {
177                         $patron->standing($s) if ( $s->id eq $ctx->{patron}->standing );
178                 }
179         }
180
181         # set the patron ptofile to the profile name
182         $patron->profile( _get_patron_profile( 
183                 $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
184
185         # flesh the org unit
186         $patron->home_ou( 
187                 $apputils->fetch_org_unit( $patron->home_ou ) ) if $patron;
188
189 }
190
191 # recurse and find the patron profile name from the tree
192 # another option would be to grab the groups for the patron
193 # and cycle through those until the "profile" group has been found
194 sub _get_patron_profile { 
195         my( $patron, $group_tree ) = @_;
196         return $group_tree if ($group_tree->id eq $patron->profile);
197         return undef unless ($group_tree->children);
198
199         for my $child (@{$group_tree->children}) {
200                 my $ret = _get_patron_profile( $patron, $child );
201                 return $ret if $ret;
202         }
203         return undef;
204 }
205
206 sub _get_copy_status {
207         my( $copy, $cstatus ) = @_;
208         my $s = undef;
209         for my $status (@$cstatus) {
210                 $s = $status if( $status->id eq $copy->status ) 
211         }
212         $logger->debug("Retrieving copy status: " . $s->name) if $s;
213         return $s;
214 }
215
216 sub _get_copy_location {
217         my( $copy, $locations ) = @_;
218         my $l = undef;
219         for my $loc (@$locations) {
220                 $l = $loc if $loc->id eq $copy->location;
221         }
222         $logger->debug("Retrieving copy location: " . $l->name ) if $l;
223         return $l;
224 }
225
226
227 # ------------------------------------------------------------------------------
228 # Constructs and shoves data into the script environment
229 # ------------------------------------------------------------------------------
230 sub _build_circ_script_runner {
231         my $ctx = shift;
232
233         $logger->debug("Loading script environment for circulation");
234
235         my $runner;
236         if( $runner = $contexts{$ctx->{type}} ) {
237                 $runner->refresh_context;
238         } else {
239                 $runner = OpenILS::Utils::ScriptRunner->new unless $runner;
240                 $contexts{type} = $runner;
241         }
242
243         for(@$script_libs) {
244                 $logger->debug("Loading circ script lib path $_");
245                 $runner->add_path( $_ );
246         }
247
248         $runner->insert( 'environment.patron',          $ctx->{patron}, 1);
249         $runner->insert( 'environment.title',           $ctx->{title}, 1);
250         $runner->insert( 'environment.copy',            $ctx->{copy}, 1);
251
252         # circ script result
253         $runner->insert( 'result', {} );
254         $runner->insert( 'result.event', 'SUCCESS' );
255
256         $runner->insert('environment.isRenewal', 1) if $ctx->{renew};
257         $runner->insert('environment.isNonCat', 1) if $ctx->{noncat};
258         $runner->insert('environment.nonCatType', $ctx->{noncat_type}) if $ctx->{noncat};
259
260         if(ref($ctx->{patron_circ_summary})) {
261                 $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
262                 $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
263         }
264
265         $ctx->{runner} = $runner;
266         return $runner;
267 }
268
269
270 sub _add_script_runner_methods {
271         my $ctx = shift;
272         my $runner = $ctx->{runner};
273
274         if( $ctx->{copy} ) {
275                 
276                 # allows a script to fetch a hold that is currently targeting the
277                 # copy in question
278                 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
279                                 my $key = shift;
280                                 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
281                                 $hold = undef unless $hold;
282                                 $runner->insert( $key, $hold, 1 );
283                         }
284                 );
285         }
286 }
287
288 # ------------------------------------------------------------------------------
289
290 __PACKAGE__->register_method(
291         method  => "permit_circ",
292         api_name        => "open-ils.circ.checkout.permit",
293         notes           => q/
294                 Determines if the given checkout can occur
295                 @param authtoken The login session key
296                 @param params A trailing hash of named params including 
297                         barcode : The copy barcode, 
298                         patron : The patron the checkout is occurring for, 
299                         renew : true or false - whether or not this is a renewal
300                 @return The event that occurred during the permit check.  
301                         If all is well, the SUCCESS event is returned
302         /);
303
304 sub permit_circ {
305         my( $self, $client, $authtoken, $params ) = @_;
306
307         my ( $requestor, $patron, $ctx, $evt );
308
309         # check permisson of the requestor
310         ( $requestor, $patron, $evt ) = 
311                 $apputils->checkses_requestor( 
312                 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
313         return $evt if $evt;
314
315         # fetch and build the circulation environment
316         ( $ctx, $evt ) = create_circ_ctx( %$params, 
317                 patron                                                  => $patron, 
318                 requestor                                               => $requestor, 
319                 type                                                            => 'permit',
320                 fetch_patron_circ_summary       => 1,
321                 fetch_copy_statuses                     => 1, 
322                 fetch_copy_locations                    => 1, 
323                 );
324         return $evt if $evt;
325
326         return _run_permit_scripts($ctx);
327 }
328
329
330 # Runs the patron and copy permit scripts
331 # if this is a non-cat circulation, the copy permit script 
332 # is not run
333 sub _run_permit_scripts {
334
335         my $ctx                 = shift;
336         my $runner              = $ctx->{runner};
337         my $patronid    = $ctx->{patron}->id;
338         my $barcode             = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
339
340         $runner->load($scripts{circ_permit_patron});
341         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
342         my $evtname = $runner->retrieve('result.event');
343         $logger->activity("circ_permit_patron for user $patronid returned event: $evtname");
344
345         return OpenILS::Event->new($evtname) 
346                 if ( $ctx->{noncat} or $evtname ne 'SUCCESS' );
347
348         $runner->load($scripts{circ_permit_copy});
349         $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
350         $evtname = $runner->retrieve('result.event');
351         $logger->activity("circ_permit_patron for user $patronid ".
352                 "and copy $barcode returned event: $evtname");
353
354         return OpenILS::Event->new($evtname);
355
356 }
357
358
359 # ------------------------------------------------------------------------------
360
361 __PACKAGE__->register_method(
362         method  => "checkout",
363         api_name        => "open-ils.circ.checkout",
364         notes => q/
365                 Checks out an item
366                 @param authtoken The login session key
367                 @param params A named hash of params including:
368                         copy                    The copy object
369                         barcode         If no copy is provided, the copy is retrieved via barcode
370                         copyid          If no copy or barcode is provide, the copy id will be use
371                         patron          The patron's id
372                         noncat          True if this is a circulation for a non-cataloted item
373                         noncat_type     The non-cataloged type id
374                         noncat_circ_lib The location for the noncat circ.  
375                                 Default is the home org of the staff member
376                 @return The SUCCESS event on success, any other event depending on the error
377         /);
378
379 sub checkout {
380         my( $self, $client, $authtoken, $params ) = @_;
381
382         my ( $requestor, $patron, $ctx, $evt, $circ );
383
384         # check permisson of the requestor
385         ( $requestor, $patron, $evt ) = 
386                 $apputils->checkses_requestor( 
387                         $authtoken, $params->{patron}, 'COPY_CHECKOUT' );
388         return $evt if $evt;
389
390         return _checkout_noncat( $requestor, $patron, %$params ) if $params->{noncat};
391
392         my $session = $U->start_db_session();
393
394         # fetch and build the circulation environment
395         ( $ctx, $evt ) = create_circ_ctx( %$params, 
396                 patron                                                  => $patron, 
397                 requestor                                               => $requestor, 
398                 session                                                 => $session, 
399                 type                                                            => 'checkout',
400                 fetch_patron_circ_summary       => 1,
401                 fetch_copy_statuses                     => 1, 
402                 fetch_copy_locations                    => 1, 
403                 );
404         return $evt if $evt;
405
406         $ctx->{circ_lib} = (defined($params->{circ_lib})) ? 
407                 $params->{circ_lib} : $requestor->home_ou;
408
409         $evt = _run_checkout_scripts($ctx);
410         return $evt if $evt;
411
412         _build_checkout_circ_object($ctx);
413
414         $evt = _commit_checkout_circ_object($ctx);
415         return $evt if $evt;
416
417         _update_checkout_copy($ctx);
418
419         $evt = _handle_related_holds($ctx);
420         return $evt if $evt;
421
422         #$U->commit_db_session($session);
423
424         return OpenILS::Event->new('SUCCESS', 
425                 payload         => { 
426                         copy            => $ctx->{copy},
427                         circ            => $ctx->{circ},
428                         record  => $U->record_to_mvr($ctx->{title}),
429                 } );
430 }
431
432
433 sub _run_checkout_scripts {
434         my $ctx = shift;
435         my $evt;
436         my $circ;
437
438         my $runner = $ctx->{runner};
439
440         $runner->insert('result.durationLevel');
441         $runner->insert('result.durationRule');
442         $runner->insert('result.recurringFinesRule');
443         $runner->insert('result.recurringFinesLevel');
444         $runner->insert('result.maxFine');
445
446         $runner->load($scripts{circ_duration});
447         $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
448         my $duration = $runner->retrieve('result.durationRule');
449         $logger->debug("Circ duration script yielded a duration rule of: $duration");
450
451         $runner->load($scripts{circ_recurring_fines});
452         $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
453         my $recurring = $runner->retrieve('result.recurringFinesRule');
454         $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
455
456         $runner->load($scripts{circ_max_fines});
457         $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
458         my $max_fine = $runner->retrieve('result.maxFine');
459         $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
460
461         ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
462         return $evt if $evt;
463         ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
464         return $evt if $evt;
465         ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
466         return $evt if $evt;
467
468         $ctx->{duration_level}                  = $runner->retrieve('result.durationLevel');
469         $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
470         $ctx->{duration_rule}                   = $duration;
471         $ctx->{recurring_fines_rule}    = $recurring;
472         $ctx->{max_fine_rule}                   = $max_fine;
473
474         return undef;
475 }
476
477 sub _build_checkout_circ_object {
478         my $ctx = shift;
479
480         my $circ                        = new Fieldmapper::action::circulation;
481         my $duration    = $ctx->{duration_rule};
482         my $max                 = $ctx->{max_fine_rule};
483         my $recurring   = $ctx->{recurring_fines_rule};
484         my $copy                        = $ctx->{copy};
485         my $patron              = $ctx->{patron};
486         my $dur_level   = $ctx->{duration_level};
487         my $rec_level   = $ctx->{recurring_fines_level};
488
489         $circ->duration( $duration->shrt ) if ($dur_level == 1);
490         $circ->duration( $duration->normal ) if ($dur_level == 2);
491         $circ->duration( $duration->extended ) if ($dur_level == 3);
492
493         $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
494         $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
495         $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
496
497         $circ->duration_rule( $duration->name );
498         $circ->recuring_fine_rule( $recurring->name );
499         $circ->max_fine_rule( $max->name );
500         $circ->max_fine( $max->amount );
501
502         $circ->fine_interval($recurring->recurance_interval);
503         $circ->renewal_remaining( $duration->max_renewals );
504         $circ->target_copy( $copy->id );
505         $circ->usr( $patron->id );
506         $circ->circ_lib( $ctx->{circ_lib} );
507
508         if( $ctx->{renew} ) {
509                 $circ->opac_renewal(1); # XXX different for different types ?????
510                 $circ->clear_id;
511                 #$circ->renewal_remaining($numrenews - 1); # XXX
512                 $circ->circ_staff($ctx->{patron}->id);
513
514         } else {
515                 $circ->circ_staff( $ctx->{requestor}->id );
516         }
517
518         _set_circ_due_date($circ);
519         $ctx->{circ} = $circ;
520 }
521
522 sub _set_circ_due_date {
523         my $circ = shift;
524
525         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = 
526                 gmtime(OpenSRF::Utils->interval_to_seconds($circ->duration) + int(time()));
527
528         $year += 1900; $mon += 1;
529         my $due_date = sprintf(
530         '%s-%0.2d-%0.2dT%s:%0.2d:%0.s2-00',
531         $year, $mon, $mday, $hour, $min, $sec);
532
533         $logger->debug("Checkout setting due date on circ to: $due_date");
534         $circ->due_date($due_date);
535 }
536
537 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
538 sub _update_checkout_copy {
539         my $ctx = shift;
540         my $copy = $ctx->{copy};
541
542         $copy->status( $copy->status->id );
543         $copy->editor( $ctx->{requestor}->id );
544         $copy->edit_date( 'now' );
545         $copy->location( $copy->location->id );
546         $copy->circ_lib( $copy->circ_lib->id );
547
548         $logger->debug("Updating editor info on copy in checkout: " . $copy->id );
549         $ctx->{session}->request( 
550                 'open-ils.storage.direct.asset.copy.update', $copy )->gather(1);
551 }
552
553 # commits the circ object to the db then fleshes the circ with rules objects
554 sub _commit_checkout_circ_object {
555
556         my $ctx = shift;
557         my $circ = $ctx->{circ};
558
559         my $r = $ctx->{session}->request(
560                 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
561
562         return $U->DB_UPDATE_FAILED($circ) unless $r;
563
564         $logger->debug("Created a new circ object in checkout: $r");
565
566         $circ->id($r);
567         $circ->duration_rule($ctx->{duration_rule});
568         $circ->max_fine_rule($ctx->{max_fine_rule});
569         $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
570
571         return undef;
572 }
573
574
575 # sees if there are any holds that this copy 
576 sub _handle_related_holds {
577
578         my $ctx         = shift;
579         my $copy                = $ctx->{copy};
580         my $patron      = $ctx->{patron};
581         my $holds       = $holdcode->fetch_related_holds($copy->id);
582
583         if(ref($holds) && @$holds) {
584
585                 # for now, just sort by id to get what should be the oldest hold
586                 $holds = [ sort { $a->id <=> $b->id } @$holds ];
587                 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
588
589                 if(@$holds) {
590                         my $hold = $holds->[0];
591
592                         $logger->debug("Related hold found in checkout: " . $hold->id );
593
594                         $hold->fulfillment_time('now');
595                         my $r = $ctx->{session}->request(
596                                 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
597                         return $U->DB_UPDATE_FAILED( $hold ) unless $r;
598                 }
599         }
600
601         return undef;
602 }
603
604
605 sub _checkout_noncat {
606         my ( $requestor, $patron, %params ) = @_;
607         my $circlib = $params{noncat_circ_lib} || $requestor->home_ou;
608         my( $circ, $evt ) = 
609                 OpenILS::Application::Circ::NonCat::create_non_cat_circ(
610                         $requestor->id, $patron->id, $circlib, $params{noncat_type} );
611         return $evt if $evt;
612         return OpenILS::Event->new('SUCCESS');
613 }
614
615
616 # ------------------------------------------------------------------------------
617
618 __PACKAGE__->register_method(
619         method  => "checkin",
620         api_name        => "open-ils.circ.checkin",
621         notes           => <<"  NOTES");
622         PARAMS( authtoken, barcode => bc )
623         Checks in based on barcode
624         Returns an event object whose payload contains the record, circ, and copy
625         If the item needs to be routed, the event is a ROUTE_COPY event
626         with an additional 'route_to' variable set on the event
627         NOTES
628
629 sub checkin {
630         my( $self, $client, $authtoken, $params ) = @_;
631 }
632
633 # ------------------------------------------------------------------------------
634
635 __PACKAGE__->register_method(
636         method  => "renew",
637         api_name        => "open-ils.circ.renew_",
638         notes           => <<"  NOTES");
639         PARAMS( authtoken, circ => circ_id );
640         open-ils.circ.renew(login_session, circ_object);
641         Renews the provided circulation.  login_session is the requestor of the
642         renewal and if the logged in user is not the same as circ->usr, then
643         the logged in user must have RENEW_CIRC permissions.
644         NOTES
645
646 sub renew {
647         my( $self, $client, $authtoken, $params ) = @_;
648 }
649
650         
651
652
653 1;