1 package OpenILS::Application::Circ::Circulate;
2 use base 'OpenSRF::Application';
3 use strict; use warnings;
4 use OpenSRF::EX qw(:try);
7 use OpenSRF::Utils::Cache;
8 use Digest::MD5 qw(md5_hex);
9 use OpenILS::Utils::ScriptRunner;
10 use OpenILS::Application::AppUtils;
11 use OpenILS::Application::Circ::Holds;
12 use OpenSRF::Utils::Logger qw(:logger);
14 $Data::Dumper::Indent = 0;
15 my $apputils = "OpenILS::Application::AppUtils";
17 my $holdcode = "OpenILS::Application::Circ::Holds";
19 my %scripts; # - circulation script filenames
20 my $script_libs; # - any additional script libraries
21 my %cache; # - db objects cache
22 my %contexts; # - Script runner contexts
23 my $cache_handle; # - memcache handle
25 # ------------------------------------------------------------------------------
26 # Load the circ script from the config
27 # ------------------------------------------------------------------------------
31 $cache_handle = OpenSRF::Utils::Cache->new('global');
32 my $conf = OpenSRF::Utils::SettingsClient->new;
33 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
34 my @pfx = ( @pfx2, "scripts" );
36 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
37 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
38 my $d = $conf->config_value( @pfx, 'circ_duration' );
39 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
40 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
41 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
42 my $ph = $conf->config_value( @pfx, 'circ_permit_hold' );
43 my $lb = $conf->config_value( @pfx2, 'script_path' );
45 $logger->error( "Missing circ script(s)" )
46 unless( $p and $c and $d and $f and $m and $pr and $ph );
48 $scripts{circ_permit_patron} = $p;
49 $scripts{circ_permit_copy} = $c;
50 $scripts{circ_duration} = $d;
51 $scripts{circ_recurring_fines}= $f;
52 $scripts{circ_max_fines} = $m;
53 $scripts{circ_renew_permit} = $pr;
54 $scripts{hold_permit} = $ph;
56 $lb = [ $lb ] unless ref($lb);
59 $logger->debug("Loaded rules scripts for circ: " .
60 "circ permit patron: $p, circ permit copy: $c, ".
61 "circ duration :$d , circ recurring fines : $f, " .
62 "circ max fines : $m, circ renew permit : $pr, permit hold: $ph");
66 # ------------------------------------------------------------------------------
67 # Loads the necessary circ objects and pushes them into the script environment
68 # Returns ( $data, $evt ). if $evt is defined, then an
69 # unexpedted event occurred and should be dealt with / returned to the caller
70 # ------------------------------------------------------------------------------
77 $evt = _ctx_add_patron_objects($ctx, %params);
80 if( ($params{copy} or $params{copyid} or $params{barcode}) and !$params{noncat} ) {
81 $evt = _ctx_add_copy_objects($ctx, %params);
85 _doctor_patron_object($ctx) if $ctx->{patron};
86 _doctor_copy_object($ctx) if $ctx->{copy};
87 _doctor_circ_objects($ctx);
88 _build_circ_script_runner($ctx);
89 _add_script_runner_methods( $ctx );
94 sub _ctx_add_patron_objects {
95 my( $ctx, %params) = @_;
97 $ctx->{patron} = $params{patron};
99 if(!defined($cache{patron_standings})) {
100 $cache{patron_standings} = $apputils->fetch_patron_standings();
101 $cache{group_tree} = $apputils->fetch_permission_group_tree();
104 $ctx->{patron_standings} = $cache{patron_standings};
105 $ctx->{group_tree} = $cache{group_tree};
107 $ctx->{patron_circ_summary} =
108 $apputils->fetch_patron_circ_summary($ctx->{patron}->id)
109 if $params{fetch_patron_circsummary};
115 sub _ctx_add_copy_objects {
116 my($ctx, %params) = @_;
119 $cache{copy_statuses} = $apputils->fetch_copy_statuses
120 if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
122 $cache{copy_locations} = $apputils->fetch_copy_locations
123 if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
125 $ctx->{copy_statuses} = $cache{copy_statuses};
126 $ctx->{copy_locations} = $cache{copy_locations};
128 my $copy = $params{copy} if $params{copy};
133 $apputils->fetch_copy($params{copyid}) if $params{copyid};
138 $apputils->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
143 $ctx->{copy} = $copy;
145 ( $ctx->{title}, $evt ) = $apputils->fetch_record_by_copy( $ctx->{copy}->id );
152 # ------------------------------------------------------------------------------
153 # Fleshes parts of the patron object
154 # ------------------------------------------------------------------------------
155 sub _doctor_copy_object {
158 my $copy = $ctx->{copy};
160 # set the copy status to a status name
161 $copy->status( _get_copy_status(
162 $copy, $ctx->{copy_statuses} ) ) if $copy;
164 # set the copy location to the location object
165 $copy->location( _get_copy_location(
166 $copy, $ctx->{copy_locations} ) ) if $copy;
168 $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
172 # ------------------------------------------------------------------------------
173 # Fleshes parts of the copy object
174 # ------------------------------------------------------------------------------
175 sub _doctor_patron_object {
177 my $patron = $ctx->{patron};
179 # push the standing object into the patron
180 if(ref($ctx->{patron_standings})) {
181 for my $s (@{$ctx->{patron_standings}}) {
182 $patron->standing($s) if ( $s->id eq $ctx->{patron}->standing );
186 # set the patron ptofile to the profile name
187 $patron->profile( _get_patron_profile(
188 $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
192 $apputils->fetch_org_unit( $patron->home_ou ) ) if $patron;
196 # recurse and find the patron profile name from the tree
197 # another option would be to grab the groups for the patron
198 # and cycle through those until the "profile" group has been found
199 sub _get_patron_profile {
200 my( $patron, $group_tree ) = @_;
201 return $group_tree if ($group_tree->id eq $patron->profile);
202 return undef unless ($group_tree->children);
204 for my $child (@{$group_tree->children}) {
205 my $ret = _get_patron_profile( $patron, $child );
211 sub _get_copy_status {
212 my( $copy, $cstatus ) = @_;
214 for my $status (@$cstatus) {
215 $s = $status if( $status->id eq $copy->status )
217 $logger->debug("Retrieving copy status: " . $s->name) if $s;
221 sub _get_copy_location {
222 my( $copy, $locations ) = @_;
224 for my $loc (@$locations) {
225 $l = $loc if $loc->id eq $copy->location;
227 $logger->debug("Retrieving copy location: " . $l->name ) if $l;
232 # ------------------------------------------------------------------------------
233 # Constructs and shoves data into the script environment
234 # ------------------------------------------------------------------------------
235 sub _build_circ_script_runner {
238 $logger->debug("Loading script environment for circulation");
241 if( $runner = $contexts{$ctx->{type}} ) {
242 $runner->refresh_context;
244 $runner = OpenILS::Utils::ScriptRunner->new unless $runner;
245 $contexts{type} = $runner;
249 $logger->debug("Loading circ script lib path $_");
250 $runner->add_path( $_ );
253 $runner->insert( 'environment.patron', $ctx->{patron}, 1);
254 $runner->insert( 'environment.title', $ctx->{title}, 1);
255 $runner->insert( 'environment.copy', $ctx->{copy}, 1);
258 $runner->insert( 'result', {} );
259 $runner->insert( 'result.event', 'SUCCESS' );
261 $runner->insert('environment.isRenewal', 1) if $ctx->{renew};
262 $runner->insert('environment.isNonCat', 1) if $ctx->{noncat};
263 $runner->insert('environment.nonCatType', $ctx->{noncat_type}) if $ctx->{noncat};
265 if(ref($ctx->{patron_circ_summary})) {
266 $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
267 $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
270 $ctx->{runner} = $runner;
275 sub _add_script_runner_methods {
277 my $runner = $ctx->{runner};
281 # allows a script to fetch a hold that is currently targeting the
283 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
285 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
286 $hold = undef unless $hold;
287 $runner->insert( $key, $hold, 1 );
293 # ------------------------------------------------------------------------------
295 __PACKAGE__->register_method(
296 method => "permit_circ",
297 api_name => "open-ils.circ.checkout.permit",
299 Determines if the given checkout can occur
300 @param authtoken The login session key
301 @param params A trailing hash of named params including
302 barcode : The copy barcode,
303 patron : The patron the checkout is occurring for,
304 renew : true or false - whether or not this is a renewal
305 @return The event that occurred during the permit check.
306 If all is well, the SUCCESS event is returned
310 my( $self, $client, $authtoken, $params ) = @_;
312 my ( $requestor, $patron, $ctx, $evt );
315 $logger->debug("PERMIT: " . Dumper($params));
318 # check permisson of the requestor
319 ( $requestor, $patron, $evt ) =
320 $apputils->checkses_requestor(
321 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
324 # fetch and build the circulation environment
325 ( $ctx, $evt ) = create_circ_ctx( %$params,
327 requestor => $requestor,
329 fetch_patron_circ_summary => 1,
330 fetch_copy_statuses => 1,
331 fetch_copy_locations => 1,
335 return _run_permit_scripts($ctx);
339 # Runs the patron and copy permit scripts
340 # if this is a non-cat circulation, the copy permit script
342 sub _run_permit_scripts {
345 my $runner = $ctx->{runner};
346 my $patronid = $ctx->{patron}->id;
347 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
349 $runner->load($scripts{circ_permit_patron});
350 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
351 my $evtname = $runner->retrieve('result.event');
352 $logger->activity("circ_permit_patron for user $patronid returned event: $evtname");
354 return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';
356 if ( $ctx->{noncat} ) {
357 my $key = _cache_permit_key(-1, $patronid, $ctx->{requestor}->id);
358 return OpenILS::Event->new($evtname, payload => $key);
361 $runner->load($scripts{circ_permit_copy});
362 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
363 $evtname = $runner->retrieve('result.event');
364 $logger->activity("circ_permit_patron for user $patronid ".
365 "and copy $barcode returned event: $evtname");
367 if( $evtname eq 'SUCCESS' ) {
368 my $key = _cache_permit_key($ctx->{copy}->id, $patronid, $ctx->{requestor}->id);
369 return OpenILS::Event->new($evtname, payload => $key);
372 return OpenILS::Event->new($evtname);
376 # takes copyid, patronid, and requestor id
377 sub _cache_permit_key {
378 my( $cid, $pid, $rid ) = @_;
379 my $key = md5_hex( time() . rand() . "$$ $cid $pid $rid" );
380 $logger->debug("Setting circ permit key [$key] for copy $cid, patron $pid, and staff $rid");
381 $cache_handle->put_cache( "oils_permit_key_$key", [ $cid, $pid, $rid ], 300 );
385 # takes permit_key, copyid, patronid, and requestor id
386 sub _check_permit_key {
387 my( $key, $cid, $pid, $rid ) = @_;
388 $logger->debug("Fetching circ permit key $key");
389 my $k = "oils_permit_key_$key";
390 my $arr = $cache_handle->get_cache($k);
391 $cache_handle->delete_cache($k);
392 return 1 if( ref($arr) and @$arr[0] eq $cid and @$arr[1] eq $pid and @$arr[2] eq $rid );
397 # ------------------------------------------------------------------------------
399 __PACKAGE__->register_method(
400 method => "checkout",
401 api_name => "open-ils.circ.checkout",
404 @param authtoken The login session key
405 @param params A named hash of params including:
407 barcode If no copy is provided, the copy is retrieved via barcode
408 copyid If no copy or barcode is provide, the copy id will be use
409 patron The patron's id
410 noncat True if this is a circulation for a non-cataloted item
411 noncat_type The non-cataloged type id
412 noncat_circ_lib The location for the noncat circ.
413 Default is the home org of the staff member
414 @return The SUCCESS event on success, any other event depending on the error
418 my( $self, $client, $authtoken, $params ) = @_;
420 my ( $requestor, $patron, $ctx, $evt, $circ );
421 my $key = $params->{permit_key};
423 # check permisson of the requestor
424 ( $requestor, $patron, $evt ) =
425 $apputils->checkses_requestor(
426 $authtoken, $params->{patron}, 'COPY_CHECKOUT' );
429 if( $params->{noncat} ) {
430 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
431 unless _check_permit_key( $key, -1, $patron->id, $requestor->id );
433 ( $circ, $evt ) = _checkout_noncat( $requestor, $patron, %$params );
435 return OpenILS::Event->new('SUCCESS',
436 payload => { noncat_circ => $circ } );
439 my $session = $U->start_db_session();
441 # fetch and build the circulation environment
442 ( $ctx, $evt ) = create_circ_ctx( %$params,
444 requestor => $requestor,
447 fetch_patron_circ_summary => 1,
448 fetch_copy_statuses => 1,
449 fetch_copy_locations => 1,
453 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
454 unless _check_permit_key( $key, $ctx->{copy}->id, $patron->id, $requestor->id );
456 $ctx->{circ_lib} = (defined($params->{circ_lib})) ?
457 $params->{circ_lib} : $requestor->home_ou;
459 $evt = _run_checkout_scripts($ctx);
462 _build_checkout_circ_object($ctx);
464 $evt = _commit_checkout_circ_object($ctx);
467 _update_checkout_copy($ctx);
469 $evt = _handle_related_holds($ctx);
472 #$U->commit_db_session($session);
473 $session->disconnect;
475 return OpenILS::Event->new('SUCCESS',
477 copy => $ctx->{copy},
478 circ => $ctx->{circ},
479 record => $U->record_to_mvr($ctx->{title}),
484 sub _run_checkout_scripts {
489 my $runner = $ctx->{runner};
491 $runner->insert('result.durationLevel');
492 $runner->insert('result.durationRule');
493 $runner->insert('result.recurringFinesRule');
494 $runner->insert('result.recurringFinesLevel');
495 $runner->insert('result.maxFine');
497 $runner->load($scripts{circ_duration});
498 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
499 my $duration = $runner->retrieve('result.durationRule');
500 $logger->debug("Circ duration script yielded a duration rule of: $duration");
502 $runner->load($scripts{circ_recurring_fines});
503 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
504 my $recurring = $runner->retrieve('result.recurringFinesRule');
505 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
507 $runner->load($scripts{circ_max_fines});
508 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
509 my $max_fine = $runner->retrieve('result.maxFine');
510 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
512 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
514 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
516 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
519 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
520 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
521 $ctx->{duration_rule} = $duration;
522 $ctx->{recurring_fines_rule} = $recurring;
523 $ctx->{max_fine_rule} = $max_fine;
528 sub _build_checkout_circ_object {
531 my $circ = new Fieldmapper::action::circulation;
532 my $duration = $ctx->{duration_rule};
533 my $max = $ctx->{max_fine_rule};
534 my $recurring = $ctx->{recurring_fines_rule};
535 my $copy = $ctx->{copy};
536 my $patron = $ctx->{patron};
537 my $dur_level = $ctx->{duration_level};
538 my $rec_level = $ctx->{recurring_fines_level};
540 $circ->duration( $duration->shrt ) if ($dur_level == 1);
541 $circ->duration( $duration->normal ) if ($dur_level == 2);
542 $circ->duration( $duration->extended ) if ($dur_level == 3);
544 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
545 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
546 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
548 $circ->duration_rule( $duration->name );
549 $circ->recuring_fine_rule( $recurring->name );
550 $circ->max_fine_rule( $max->name );
551 $circ->max_fine( $max->amount );
553 $circ->fine_interval($recurring->recurance_interval);
554 $circ->renewal_remaining( $duration->max_renewals );
555 $circ->target_copy( $copy->id );
556 $circ->usr( $patron->id );
557 $circ->circ_lib( $ctx->{circ_lib} );
559 if( $ctx->{renew} ) {
560 $circ->opac_renewal(1); # XXX different for different types ?????
562 #$circ->renewal_remaining($numrenews - 1); # XXX
563 $circ->circ_staff($ctx->{patron}->id);
566 $circ->circ_staff( $ctx->{requestor}->id );
569 _set_circ_due_date($circ);
570 $ctx->{circ} = $circ;
573 sub _create_due_date {
574 my $duration = shift;
576 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
577 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
579 $year += 1900; $mon += 1;
580 my $due_date = sprintf(
581 '%s-%0.2d-%0.2dT%s:%0.2d:%0.s2-00',
582 $year, $mon, $mday, $hour, $min, $sec);
586 sub _set_circ_due_date {
588 my $dd = _create_due_date($circ->duration);
589 $logger->debug("Checkout setting due date on circ to: $dd");
590 $circ->due_date($dd);
593 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
594 sub _update_checkout_copy {
596 my $copy = $ctx->{copy};
598 $copy->status( $copy->status->id );
599 $copy->editor( $ctx->{requestor}->id );
600 $copy->edit_date( 'now' );
601 $copy->location( $copy->location->id );
602 $copy->circ_lib( $copy->circ_lib->id );
604 $logger->debug("Updating editor info on copy in checkout: " . $copy->id );
605 $ctx->{session}->request(
606 'open-ils.storage.direct.asset.copy.update', $copy )->gather(1);
609 # commits the circ object to the db then fleshes the circ with rules objects
610 sub _commit_checkout_circ_object {
613 my $circ = $ctx->{circ};
615 my $r = $ctx->{session}->request(
616 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
618 return $U->DB_UPDATE_FAILED($circ) unless $r;
620 $logger->debug("Created a new circ object in checkout: $r");
623 $circ->duration_rule($ctx->{duration_rule});
624 $circ->max_fine_rule($ctx->{max_fine_rule});
625 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
631 # sees if there are any holds that this copy
632 sub _handle_related_holds {
635 my $copy = $ctx->{copy};
636 my $patron = $ctx->{patron};
637 my $holds = $holdcode->fetch_related_holds($copy->id);
639 if(ref($holds) && @$holds) {
641 # for now, just sort by id to get what should be the oldest hold
642 $holds = [ sort { $a->id <=> $b->id } @$holds ];
643 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
646 my $hold = $holds->[0];
648 $logger->debug("Related hold found in checkout: " . $hold->id );
650 $hold->fulfillment_time('now');
651 my $r = $ctx->{session}->request(
652 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
653 return $U->DB_UPDATE_FAILED( $hold ) unless $r;
661 sub _checkout_noncat {
662 my ( $requestor, $patron, %params ) = @_;
663 my $circlib = $params{noncat_circ_lib} || $requestor->home_ou;
664 return OpenILS::Application::Circ::NonCat::create_non_cat_circ(
665 $requestor->id, $patron->id, $circlib, $params{noncat_type} );
669 # ------------------------------------------------------------------------------
671 __PACKAGE__->register_method(
673 api_name => "open-ils.circ.checkin",
674 notes => <<" NOTES");
675 PARAMS( authtoken, barcode => bc )
676 Checks in based on barcode
677 Returns an event object whose payload contains the record, circ, and copy
678 If the item needs to be routed, the event is a ROUTE_COPY event
679 with an additional 'route_to' variable set on the event
683 my( $self, $client, $authtoken, $params ) = @_;
686 # ------------------------------------------------------------------------------
688 __PACKAGE__->register_method(
690 api_name => "open-ils.circ.renew_",
691 notes => <<" NOTES");
692 PARAMS( authtoken, circ => circ_id );
693 open-ils.circ.renew(login_session, circ_object);
694 Renews the provided circulation. login_session is the requestor of the
695 renewal and if the logged in user is not the same as circ->usr, then
696 the logged in user must have RENEW_CIRC permissions.
700 my( $self, $client, $authtoken, $params ) = @_;