1 package OpenILS::Application::Circ::Circulate;
2 use base 'OpenSRF::Application';
3 use strict; use warnings;
4 use OpenSRF::EX qw(:try);
6 use OpenSRF::Utils::Cache;
7 use OpenSRF::AppSession;
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 OpenILS::Application::Circ::Transit;
13 use OpenILS::Utils::PermitHold;
14 use OpenSRF::Utils::Logger qw(:logger);
16 use DateTime::Format::ISO8601;
17 use OpenSRF::Utils qw/:datetime/;
19 $Data::Dumper::Indent = 0;
20 my $apputils = "OpenILS::Application::AppUtils";
22 my $holdcode = "OpenILS::Application::Circ::Holds";
23 my $transcode = "OpenILS::Application::Circ::Transit";
25 my %scripts; # - circulation script filenames
26 my $script_libs; # - any additional script libraries
27 my %cache; # - db objects cache
28 my %contexts; # - Script runner contexts
29 my $cache_handle; # - memcache handle
31 sub PRECAT_FINE_LEVEL { return 2; }
32 sub PRECAT_LOAN_DURATION { return 2; }
34 my %RECORD_FROM_COPY_CACHE;
37 # for security, this is a process-defined and not
38 # a client-defined variable
41 # ------------------------------------------------------------------------------
42 # Load the circ script from the config
43 # ------------------------------------------------------------------------------
47 $cache_handle = OpenSRF::Utils::Cache->new('global');
48 my $conf = OpenSRF::Utils::SettingsClient->new;
49 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
50 my @pfx = ( @pfx2, "scripts" );
52 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
53 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
54 my $d = $conf->config_value( @pfx, 'circ_duration' );
55 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
56 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
57 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
58 my $lb = $conf->config_value( @pfx2, 'script_path' );
60 $logger->error( "Missing circ script(s)" )
61 unless( $p and $c and $d and $f and $m and $pr );
63 $scripts{circ_permit_patron} = $p;
64 $scripts{circ_permit_copy} = $c;
65 $scripts{circ_duration} = $d;
66 $scripts{circ_recurring_fines}= $f;
67 $scripts{circ_max_fines} = $m;
68 $scripts{circ_permit_renew} = $pr;
70 $lb = [ $lb ] unless ref($lb);
73 $logger->debug("Loaded rules scripts for circ: " .
74 "circ permit patron: $p, circ permit copy: $c, ".
75 "circ duration :$d , circ recurring fines : $f, " .
76 "circ max fines : $m, circ renew permit : $pr");
80 # ------------------------------------------------------------------------------
81 # Loads the necessary circ objects and pushes them into the script environment
82 # Returns ( $data, $evt ). if $evt is defined, then an
83 # unexpedted event occurred and should be dealt with / returned to the caller
84 # ------------------------------------------------------------------------------
92 $evt = _ctx_add_patron_objects($ctx, %params);
93 return (undef,$evt) if $evt;
95 if(!$params{noncat}) {
96 if( $evt = _ctx_add_copy_objects($ctx, %params) ) {
97 $ctx->{precat} = 1 if($evt->{textcode} eq 'COPY_NOT_FOUND')
99 $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
103 _doctor_patron_object($ctx) if $ctx->{patron};
104 _doctor_copy_object($ctx) if $ctx->{copy};
106 if(!$ctx->{no_runner}) {
107 _build_circ_script_runner($ctx);
108 _add_script_runner_methods($ctx);
114 sub _ctx_add_patron_objects {
115 my( $ctx, %params) = @_;
118 # - patron standings are now handled in the penalty server...
120 #if(!defined($cache{patron_standings})) {
121 # $cache{patron_standings} = $U->fetch_patron_standings();
123 #$ctx->{patron_standings} = $cache{patron_standings};
125 $cache{group_tree} = $U->fetch_permission_group_tree() unless $cache{group_tree};
126 $ctx->{group_tree} = $cache{group_tree};
128 $ctx->{patron_circ_summary} =
129 $U->fetch_patron_circ_summary($ctx->{patron}->id)
130 if $params{fetch_patron_circsummary};
136 sub _find_copy_by_attr {
141 my $copy = $params{copy} || undef;
146 $U->fetch_copy($params{copyid}) if $params{copyid};
147 return (undef,$evt) if $evt;
151 $U->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
152 return (undef,$evt) if $evt;
155 return ( $copy, $evt );
158 sub _ctx_add_copy_objects {
159 my($ctx, %params) = @_;
164 $cache{copy_statuses} = $U->fetch_copy_statuses
165 if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
167 $cache{copy_locations} = $U->fetch_copy_locations
168 if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
170 $ctx->{copy_statuses} = $cache{copy_statuses};
171 $ctx->{copy_locations} = $cache{copy_locations};
173 ($copy, $evt) = _find_copy_by_attr(%params);
176 if( $copy and !$ctx->{title} ) {
177 $logger->debug("Copy status: " . $copy->status);
179 my $r = $RECORD_FROM_COPY_CACHE{$copy->id};
180 ($r, $evt) = $U->fetch_record_by_copy( $copy->id ) unless $r;
182 $RECORD_FROM_COPY_CACHE{$copy->id} = $r;
185 $ctx->{copy} = $copy;
192 # ------------------------------------------------------------------------------
193 # Fleshes parts of the patron object
194 # ------------------------------------------------------------------------------
195 sub _doctor_copy_object {
198 my $copy = $ctx->{copy} || return undef;
200 $logger->debug("Doctoring copy object...");
202 # set the copy status to a status name
203 $copy->status( _get_copy_status( $copy, $ctx->{copy_statuses} ) );
205 # set the copy location to the location object
206 $copy->location( _get_copy_location( $copy, $ctx->{copy_locations} ) );
208 $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
212 # ------------------------------------------------------------------------------
213 # Fleshes parts of the patron object
214 # ------------------------------------------------------------------------------
215 sub _doctor_patron_object {
218 my $patron = $ctx->{patron} || return undef;
220 # push the standing object into the patron
221 # if(ref($ctx->{patron_standings})) {
222 # for my $s (@{$ctx->{patron_standings}}) {
223 # if( $s->id eq $ctx->{patron}->standing ) {
224 # $patron->standing($s);
225 # $logger->debug("Set patron standing to ". $s->value);
230 # set the patron ptofile to the profile name
231 $patron->profile( _get_patron_profile(
232 $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
236 $U->fetch_org_unit( $patron->home_ou ) ) if $patron;
240 # recurse and find the patron profile name from the tree
241 # another option would be to grab the groups for the patron
242 # and cycle through those until the "profile" group has been found
243 sub _get_patron_profile {
244 my( $patron, $group_tree ) = @_;
245 return $group_tree if ($group_tree->id eq $patron->profile);
246 return undef unless ($group_tree->children);
248 for my $child (@{$group_tree->children}) {
249 my $ret = _get_patron_profile( $patron, $child );
255 sub _get_copy_status {
256 my( $copy, $cstatus ) = @_;
259 for my $status (@$cstatus) {
260 $s = $status if( $status->id eq $copy->status )
262 $logger->debug("Retrieving copy status: " . $s->name) if $s;
266 sub _get_copy_location {
267 my( $copy, $locations ) = @_;
270 for my $loc (@$locations) {
271 $l = $loc if $loc->id eq $copy->location;
273 $logger->debug("Retrieving copy location: " . $l->name ) if $l;
278 # ------------------------------------------------------------------------------
279 # Constructs and shoves data into the script environment
280 # ------------------------------------------------------------------------------
281 sub _build_circ_script_runner {
285 $logger->debug("Loading script environment for circulation");
288 if( $runner = $contexts{$ctx->{type}} ) {
289 $runner->refresh_context;
291 $runner = OpenILS::Utils::ScriptRunner->new;
292 $contexts{type} = $runner;
296 $logger->debug("Loading circ script lib path $_");
297 $runner->add_path( $_ );
300 # Note: inserting the number 0 into the script turns into the
301 # string "0", and thus evaluates to true in JS land
302 # inserting undef will insert "", which evaluates to false
304 $runner->insert( 'environment.patron', $ctx->{patron}, 1);
305 $runner->insert( 'environment.title', $ctx->{title}, 1);
306 $runner->insert( 'environment.copy', $ctx->{copy}, 1);
309 $runner->insert( 'result', {} );
310 #$runner->insert( 'result.event', 'SUCCESS' );
311 $runner->insert( 'result.events', [] );
314 $runner->insert('environment.isRenewal', 1);
316 $runner->insert('environment.isRenewal', undef);
319 if($ctx->{ishold} ) {
320 $runner->insert('environment.isHold', 1);
322 $runner->insert('environment.isHold', undef)
325 if( $ctx->{noncat} ) {
326 $runner->insert('environment.isNonCat', 1);
327 $runner->insert('environment.nonCatType', $ctx->{noncat_type});
329 $runner->insert('environment.isNonCat', undef);
332 # if(ref($ctx->{patron_circ_summary})) {
333 # $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
334 # $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
337 $ctx->{runner} = $runner;
342 sub _add_script_runner_methods {
345 my $runner = $ctx->{runner};
349 # allows a script to fetch a hold that is currently targeting the
351 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
353 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
354 $hold = undef unless $hold;
355 $runner->insert( $key, $hold, 1 );
361 # ------------------------------------------------------------------------------
363 __PACKAGE__->register_method(
364 method => "permit_circ",
365 api_name => "open-ils.circ.checkout.permit",
367 Determines if the given checkout can occur
368 @param authtoken The login session key
369 @param params A trailing hash of named params including
370 barcode : The copy barcode,
371 patron : The patron the checkout is occurring for,
372 renew : true or false - whether or not this is a renewal
373 @return The event that occurred during the permit check.
376 __PACKAGE__->register_method (
377 method => 'permit_circ',
378 api_name => 'open-ils.circ.checkout.permit.override',
379 signature => q/@see open-ils.circ.checkout.permit/,
383 my( $self, $client, $authtoken, $params ) = @_;
386 my $override = $params->{override} = 1 if $self->api_name =~ /override/o;
388 my ( $requestor, $patron, $ctx, $evt, $circ );
390 # check permisson of the requestor
391 ( $requestor, $patron, $evt ) =
392 $U->checkses_requestor(
393 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
396 # fetch and build the circulation environment
397 if( !( $ctx = $params->{_ctx}) ) {
399 ( $ctx, $evt ) = create_circ_ctx( %$params,
401 requestor => $requestor,
403 #fetch_patron_circ_summary => 1,
404 fetch_copy_statuses => 1,
405 fetch_copy_locations => 1,
410 $ctx->{authtoken} = $authtoken;
413 if( $ctx->{copy} and ($evt = _handle_claims_returned($ctx)) ) {
414 return $evt unless $U->event_equals($evt, 'SUCCESS');
422 # no claims returned circ was found, check if there is any open circ
423 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
424 ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
425 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
430 $ctx->{permit_key} = _cache_permit_key();
431 my $events = _run_permit_scripts($ctx);
434 $evt = override_events($requestor, $requestor->ws_ou, $events);
436 return OpenILS::Event->new('SUCCESS', payload => $ctx->{permit_key} );
442 sub override_events {
444 my( $requestor, $org, $events ) = @_;
445 $events = [ $events ] unless ref($events) eq 'ARRAY';
448 for my $e (@$events) {
449 my $tc = $e->{textcode};
450 next if $tc eq 'SUCCESS';
451 my $ov = "$tc.override";
452 $logger->info("attempting to override event $ov");
453 my $evt = $U->check_perms( $requestor->id, $org, $ov );
461 __PACKAGE__->register_method(
462 method => "check_title_hold",
463 api_name => "open-ils.circ.title_hold.is_possible",
465 Determines if a hold were to be placed by a given user,
466 whether or not said hold would have any potential copies
468 @param authtoken The login session key
469 @param params A hash of named params including:
470 patronid - the id of the hold recipient
471 titleid (brn) - the id of the title to be held
472 depth - the hold range depth (defaults to 0)
475 sub check_title_hold {
476 my( $self, $client, $authtoken, $params ) = @_;
477 my %params = %$params;
478 my $titleid = $params{titleid};
480 my ( $requestor, $patron, $evt ) = $U->checkses_requestor(
481 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
484 my $rangelib = $patron->home_ou;
485 my $depth = $params{depth} || 0;
487 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
489 my $org = $U->simplereq(
491 'open-ils.actor.org_unit.retrieve',
492 $authtoken, $requestor->home_ou );
498 while( $title = $U->storagereq(
499 'open-ils.storage.biblio.record_entry.ranged_tree',
500 $titleid, $rangelib, $depth, $limit, $offset ) ) {
502 last unless ref($title);
504 for my $cn (@{$title->call_numbers}) {
506 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
508 for my $copy (@{$cn->copies}) {
510 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
512 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
514 requestor => $requestor,
517 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
518 request_lib => $org } );
520 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
531 # Runs the patron and copy permit scripts
532 # if this is a non-cat circulation, the copy permit script
534 sub _run_permit_scripts {
537 my $runner = $ctx->{runner};
538 my $patronid = $ctx->{patron}->id;
539 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
540 my $key = $ctx->{permit_key};
542 my $penalties = $U->update_patron_penalties(
543 authtoken => $ctx->{authtoken},
544 patron => $ctx->{patron}
547 $penalties = $penalties->{fatal_penalties};
549 $logger->info("circ patron penalties user $patronid: @$penalties");
551 if( $ctx->{noncat} ) {
552 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
553 return OpenILS::Event->new('SUCCESS', payload => $key);
557 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
558 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
562 $logger->debug("Exiting circ permit early because request is for hold patron permit");
563 return OpenILS::Event->new('SUCCESS');
566 $runner->load($scripts{circ_permit_copy});
567 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
569 # ---------------------------------------------------------------------
570 # Capture all of the copy permit events
571 # ---------------------------------------------------------------------
572 my $copy_events = $runner->retrieve('result.events');
573 $copy_events = [ split(/,/, $copy_events) ];
574 $ctx->{circ_permit_copy_events} = $copy_events;
575 $logger->activity("circ_permit_copy for copy ".
576 "$barcode returned events: @$copy_events") if @$copy_events;
579 push( @allevents, OpenILS::Event->new($_)) for @$penalties;
580 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
582 my $ae = _check_copy_alert($ctx->{copy});
583 push( @allevents, $ae ) if $ae;
585 return OpenILS::Event->new('SUCCESS', payload => $key) unless (@allevents);
587 # uniquify the events
588 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
589 @allevents = values %hash;
592 $_->{payload} = $ctx->{copy}->status->id
593 if ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
599 sub _check_copy_alert {
601 return OpenILS::Event->new('COPY_ALERT_MESSAGE',
602 payload => $copy->alert_message) if $copy->alert_message;
606 # takes copyid, patronid, and requestor id
607 sub _cache_permit_key {
608 my $key = md5_hex( time() . rand() . "$$" );
609 $logger->debug("Setting circ permit key to $key");
610 $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
614 sub _check_permit_key {
616 $logger->debug("Fetching circ permit key $key");
617 my $k = "oils_permit_key_$key";
618 my $one = $cache_handle->get_cache($k);
619 $cache_handle->delete_cache($k);
620 return ($one) ? 1 : 0;
624 # ------------------------------------------------------------------------------
626 __PACKAGE__->register_method(
627 method => "checkout",
628 api_name => "open-ils.circ.checkout",
631 @param authtoken The login session key
632 @param params A named hash of params including:
634 barcode If no copy is provided, the copy is retrieved via barcode
635 copyid If no copy or barcode is provide, the copy id will be use
636 patron The patron's id
637 noncat True if this is a circulation for a non-cataloted item
638 noncat_type The non-cataloged type id
639 noncat_circ_lib The location for the noncat circ.
640 precat The item has yet to be cataloged
641 dummy_title The temporary title of the pre-cataloded item
642 dummy_author The temporary authr of the pre-cataloded item
643 Default is the home org of the staff member
644 @return The SUCCESS event on success, any other event depending on the error
648 my( $self, $client, $authtoken, $params ) = @_;
651 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
652 my $key = $params->{permit_key};
654 # if this is a renewal, then the requestor does not have to
655 # have checkout privelages
656 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
657 ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
660 if( $params->{patron} ) {
661 ( $patron, $evt ) = $U->fetch_user($params->{patron});
664 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
668 # set the circ lib to the home org of the requestor if not specified
669 my $circlib = (defined($params->{circ_lib})) ?
670 $params->{circ_lib} : $requestor->ws_ou;
673 # Make sure the caller has a valid permit key or is
674 # overriding the permit can
675 if( $params->{permit_override} ) {
676 $evt = $U->check_perms(
677 $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
681 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
682 unless _check_permit_key($key);
685 # if this is a non-cataloged item, check it out and return
686 return _checkout_noncat(
687 $key, $requestor, $patron, %$params ) if $params->{noncat};
689 # if this item has yet to be cataloged, make sure a dummy copy exists
690 ( $params->{copy}, $evt ) = _make_precat_copy(
691 $requestor, $circlib, $params ) if $params->{precat};
695 # fetch and build the circulation environment
696 if( !( $ctx = $params->{_ctx}) ) {
697 ( $ctx, $evt ) = create_circ_ctx( %$params,
699 requestor => $requestor,
700 session => $U->start_db_session(),
702 #fetch_patron_circ_summary => 1,
703 fetch_copy_statuses => 1,
704 fetch_copy_locations => 1,
708 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
710 # if the call doesn't know it's not cataloged..
711 if(!$params->{precat}) {
712 if( $ctx->{copy}->call_number eq '-1' ) {
713 return OpenILS::Event->new('ITEM_NOT_CATALOGED');
717 # this happens in permit.. but we need to check here for 'offline' requests
718 ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
719 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
721 my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
724 $ctx->{circ_lib} = $circlib;
726 $evt = _run_checkout_scripts($ctx);
730 _build_checkout_circ_object($ctx);
732 $evt = _apply_modified_due_date($ctx);
735 $evt = _commit_checkout_circ_object($ctx);
738 $evt = _update_checkout_copy($ctx);
742 ($holds, $evt) = _handle_related_holds($ctx);
746 $logger->debug("Checkout committing objects with session thread trace: ".$ctx->{session}->session_id);
747 $U->commit_db_session($ctx->{session});
748 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
750 $logger->activity("user ".$requestor->id." successfully checked out item ".
751 $ctx->{copy}->barcode." to user ".$ctx->{patron}->id );
754 # ------------------------------------------------------------------------------
755 # Update the patron penalty info in the DB
756 # ------------------------------------------------------------------------------
757 $U->update_patron_penalties(
758 authtoken => $authtoken,
759 patron => $ctx->{patron} ,
763 return OpenILS::Event->new('SUCCESS',
765 copy => $U->unflesh_copy($ctx->{copy}),
766 circ => $ctx->{circ},
768 holds_fulfilled => $holds,
774 sub _make_precat_copy {
775 my ( $requestor, $circlib, $params ) = @_;
777 my( $copy, undef ) = _find_copy_by_attr(%$params);
780 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
782 $copy->editor($requestor->id);
783 $copy->edit_date('now');
784 $copy->dummy_title($params->{dummy_title});
785 $copy->dummy_author($params->{dummy_author});
787 my $stat = $U->storagereq(
788 'open-ils.storage.direct.asset.copy.update', $copy );
790 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $stat;
794 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
796 my $evt = OpenILS::Event->new(
797 'BAD_PARAMS', desc => "Dummy title or author not provided" )
798 unless ( $params->{dummy_title} and $params->{dummy_author} );
799 return (undef, $evt) if $evt;
801 $copy = Fieldmapper::asset::copy->new;
802 $copy->circ_lib($circlib);
803 $copy->creator($requestor->id);
804 $copy->editor($requestor->id);
805 $copy->barcode($params->{barcode});
806 $copy->call_number(-1); #special CN for precat materials
807 $copy->loan_duration(&PRECAT_LOAN_DURATION);
808 $copy->fine_level(&PRECAT_FINE_LEVEL);
810 $copy->dummy_title($params->{dummy_title});
811 $copy->dummy_author($params->{dummy_author});
813 my $id = $U->storagereq(
814 'open-ils.storage.direct.asset.copy.create', $copy );
815 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
817 $logger->debug("Pre-cataloged copy successfully created");
818 return ($U->fetch_copy($id));
822 sub _run_checkout_scripts {
828 my $runner = $ctx->{runner};
830 $runner->insert('result.durationLevel');
831 $runner->insert('result.durationRule');
832 $runner->insert('result.recurringFinesRule');
833 $runner->insert('result.recurringFinesLevel');
834 $runner->insert('result.maxFine');
836 $runner->load($scripts{circ_duration});
837 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
838 my $duration = $runner->retrieve('result.durationRule');
839 $logger->debug("Circ duration script yielded a duration rule of: $duration");
841 $runner->load($scripts{circ_recurring_fines});
842 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
843 my $recurring = $runner->retrieve('result.recurringFinesRule');
844 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
846 $runner->load($scripts{circ_max_fines});
847 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
848 my $max_fine = $runner->retrieve('result.maxFine');
849 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
851 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
853 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
855 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
858 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
859 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
860 $ctx->{duration_rule} = $duration;
861 $ctx->{recurring_fines_rule} = $recurring;
862 $ctx->{max_fine_rule} = $max_fine;
867 sub _build_checkout_circ_object {
871 my $circ = new Fieldmapper::action::circulation;
872 my $duration = $ctx->{duration_rule};
873 my $max = $ctx->{max_fine_rule};
874 my $recurring = $ctx->{recurring_fines_rule};
875 my $copy = $ctx->{copy};
876 my $patron = $ctx->{patron};
877 my $dur_level = $ctx->{duration_level};
878 my $rec_level = $ctx->{recurring_fines_level};
880 $circ->duration( $duration->shrt ) if ($dur_level == 1);
881 $circ->duration( $duration->normal ) if ($dur_level == 2);
882 $circ->duration( $duration->extended ) if ($dur_level == 3);
884 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
885 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
886 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
888 $circ->duration_rule( $duration->name );
889 $circ->recuring_fine_rule( $recurring->name );
890 $circ->max_fine_rule( $max->name );
891 $circ->max_fine( $max->amount );
893 $circ->fine_interval($recurring->recurance_interval);
894 $circ->renewal_remaining( $duration->max_renewals );
895 $circ->target_copy( $copy->id );
896 $circ->usr( $patron->id );
897 $circ->circ_lib( $ctx->{circ_lib} );
900 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
901 $circ->opac_renewal(1);
902 $circ->renewal_remaining($ctx->{renewal_remaining});
903 $circ->circ_staff($ctx->{requestor}->id);
907 # if the user provided an overiding checkout time,
908 # (e.g. the checkout really happened several hours ago), then
909 # we apply that here. Does this need a perm??
910 if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
911 $logger->debug("circ setting checkout_time to $ds");
912 $circ->xact_start($ds);
915 # if a patron is renewing, 'requestor' will be the patron
916 $circ->circ_staff($ctx->{requestor}->id );
917 _set_circ_due_date($circ);
918 $ctx->{circ} = $circ;
921 sub _apply_modified_due_date {
923 my $circ = $ctx->{circ};
925 if( $ctx->{due_date} ) {
927 my $evt = $U->check_perms(
928 $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
931 my $ds = _create_date_stamp($ctx->{due_date});
932 $logger->debug("circ modifying due_date to $ds");
933 $circ->due_date($ds);
939 sub _create_date_stamp {
940 my $datestring = shift;
941 return undef unless $datestring;
942 $datestring = clense_ISO8601($datestring);
943 $logger->debug("circ created date stamp => $datestring");
947 sub _create_due_date {
948 my $duration = shift;
950 my ($sec,$min,$hour,$mday,$mon,$year) =
951 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
952 $year += 1900; $mon += 1;
953 my $due_date = sprintf(
954 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
955 $year, $mon, $mday, $hour, $min, $sec);
959 sub _set_circ_due_date {
962 my $dd = _create_due_date($circ->duration);
963 $logger->debug("Checkout setting due date on circ to: $dd");
964 $circ->due_date($dd);
967 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
968 sub _update_checkout_copy {
971 my $copy = $ctx->{copy};
973 my $s = $U->copy_status_from_name('checked out');
974 $copy->status( $s->id ) if $s;
976 my $evt = $U->update_copy( session => $ctx->{session},
977 copy => $copy, editor => $ctx->{requestor}->id );
978 return (undef,$evt) if $evt;
983 # commits the circ object to the db then fleshes the circ with rules objects
984 sub _commit_checkout_circ_object {
987 my $circ = $ctx->{circ};
991 my $r = $ctx->{session}->request(
992 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
994 return $U->DB_UPDATE_FAILED($circ) unless $r;
996 $logger->debug("Created a new circ object in checkout: $r");
999 $circ->duration_rule($ctx->{duration_rule});
1000 $circ->max_fine_rule($ctx->{max_fine_rule});
1001 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
1007 # sees if there are any holds that this copy
1008 sub _handle_related_holds {
1011 my $copy = $ctx->{copy};
1012 my $patron = $ctx->{patron};
1013 my $holds = $holdcode->fetch_related_holds($copy->id);
1017 # XXX We should only fulfill one hold here...
1018 # XXX If a hold was transited to the user who is checking out
1019 # the item, we need to make sure that hold is what's grabbed
1020 if(ref($holds) && @$holds) {
1022 # for now, just sort by id to get what should be the oldest hold
1023 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1024 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
1027 my $hold = $holds->[0];
1029 $logger->debug("Related hold found in checkout: " . $hold->id );
1031 $hold->current_copy($copy->id); # just make sure it's set
1032 # if the hold was never officially captured, capture it.
1033 $hold->capture_time('now') unless $hold->capture_time;
1034 $hold->fulfillment_time('now');
1035 my $r = $ctx->{session}->request(
1036 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
1037 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
1038 push( @fulfilled, $hold->id );
1042 return (\@fulfilled, undef);
1045 sub _checkout_noncat {
1046 my ( $key, $requestor, $patron, %params ) = @_;
1047 my( $circ, $circlib, $evt );
1050 $circlib = $params{noncat_circ_lib} || $requestor->ws_ou;
1052 my $count = $params{noncat_count} || 1;
1053 my $cotime = _create_date_stamp($params{checkout_time}) || "";
1054 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1056 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1057 $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
1058 return $evt if $evt;
1061 return OpenILS::Event->new(
1062 'SUCCESS', payload => { noncat_circ => $circ } );
1066 __PACKAGE__->register_method(
1067 method => "generic_receive",
1068 api_name => "open-ils.circ.checkin",
1071 Generic super-method for handling all copies
1072 @param authtoken The login session key
1073 @param params Hash of named parameters including:
1074 barcode - The copy barcode
1075 force - If true, copies in bad statuses will be checked in and give good statuses
1080 __PACKAGE__->register_method(
1081 method => "generic_receive",
1082 api_name => "open-ils.circ.checkin.override",
1083 signature => q/@see open-ils.circ.checkin/
1086 sub generic_receive {
1087 my( $self, $conn, $authtoken, $params ) = @_;
1088 my( $ctx, $requestor, $evt );
1090 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1091 ( $requestor, $evt ) = $U->checksesperm(
1092 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1093 return $evt if $evt;
1095 # load up the circ objects
1096 if( !( $ctx = $params->{_ctx}) ) {
1097 ( $ctx, $evt ) = create_circ_ctx( %$params,
1098 requestor => $requestor,
1099 session => $U->start_db_session(),
1101 fetch_copy_statuses => 1,
1102 fetch_copy_locations => 1,
1105 return $evt if $evt;
1107 $ctx->{override} = 1 if $self->api_name =~ /override/o;
1108 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1109 $ctx->{authtoken} = $authtoken;
1110 my $session = $ctx->{session};
1112 my $copy = $ctx->{copy};
1113 $U->unflesh_copy($copy);
1114 return OpenILS::Event->new('COPY_NOT_FOUND') unless $copy;
1116 $logger->info("Checkin copy called by user ".
1117 $requestor->id." for copy ".$copy->id);
1119 # ------------------------------------------------------------------------------
1120 # Update the patron penalty info in the DB
1121 # ------------------------------------------------------------------------------
1122 $U->update_patron_penalties(
1123 authtoken => $authtoken,
1124 patron => $ctx->{patron},
1128 return $self->checkin_do_receive($conn, $ctx);
1131 sub checkin_do_receive {
1133 my( $self, $connection, $ctx ) = @_;
1136 my $copy = $ctx->{copy};
1137 my $session = $ctx->{session};
1138 my $requestor = $ctx->{requestor};
1139 my $change = 0; # did we actually do anything?
1144 # does the copy have an attached alert message?
1145 my $ae = _check_copy_alert($copy);
1146 push(@eventlist, $ae) if $ae;
1148 # is the copy is an a status we can't automatically resolve?
1149 $evt = _checkin_check_copy_status($ctx);
1150 push( @eventlist, $evt ) if $evt;
1153 # - see if the copy has an open circ attached
1154 ($ctx->{circ}, $evt) = $U->fetch_open_circulation($copy->id);
1155 return $evt if ($evt and $__isrenewal); # renewals require a circulation
1157 $circ = $ctx->{circ};
1159 # if the circ is marked as 'claims returned', add the event to the list
1160 push( @eventlist, 'CIRC_CLAIMS_RETURNED' )
1161 if ($circ and $circ->stop_fines eq 'CLAIMSRETURNED');
1165 if($ctx->{override}) {
1166 $evt = override_events($requestor, $requestor->ws_ou, \@eventlist );
1167 return $evt if $evt;
1173 ($ctx->{transit}) = $U->fetch_open_transit_by_copy($copy->id);
1175 if( $ctx->{circ} ) {
1177 # There is an open circ on this item, close it out.
1179 $evt = _checkin_handle_circ($ctx);
1180 return $evt if $evt;
1182 } elsif( $ctx->{transit} ) {
1184 # is this item currently in transit?
1186 $evt = $transcode->transit_receive( $copy, $requestor, $session );
1187 my $holdtrans = $evt->{holdtransit};
1188 ($ctx->{hold}) = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1190 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1192 # either an error occurred or a ROUTE_ITEM was generated and the
1193 # item must be forwarded on to its destination.
1194 return _checkin_flesh_event($ctx, $evt);
1198 # Transit has been closed, now let's see if the copy's original
1199 # status is something the staff should be warned of
1200 my $e = _checkin_check_copy_status($ctx);
1205 # copy was received as a hold transit. Copy is at target lib
1206 # and hold transit is complete. We're done here...
1207 $U->commit_db_session($session);
1208 return _checkin_flesh_event($ctx, $evt);
1214 # ------------------------------------------------------------------------------
1215 # Circulations and transits are now closed where necessary. Now go on to see if
1216 # this copy can fulfill a hold or needs to be routed to a different location
1217 # ------------------------------------------------------------------------------
1220 # If it's a renewal, we're done
1222 $U->commit_db_session($session);
1223 return OpenILS::Event->new('SUCCESS');
1226 # Now, let's see if this copy is needed for a hold
1227 my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor );
1231 $ctx->{hold} = $hold;
1234 # Capture the hold with this copy
1235 return $evt if ($evt = _checkin_capture_hold($ctx));
1237 if( $hold->pickup_lib == $requestor->ws_ou ) {
1239 # This hold was captured in the correct location
1240 $evt = OpenILS::Event->new('SUCCESS');
1244 # Hold needs to be picked up elsewhere. Build a hold
1245 # transit and route the item.
1246 return $evt if ($evt =_checkin_build_hold_transit($ctx));
1247 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1250 } else { # not needed for a hold
1252 if( $copy->circ_lib == $requestor->ws_ou ) {
1254 # Copy is in the right place.
1255 $evt = OpenILS::Event->new('SUCCESS');
1257 # if the item happens to be a pre-cataloged item, send it
1258 # to cataloging and return the event
1259 my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1260 return $err if $err;
1266 # Copy wants to go home. Transit it there.
1267 return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1268 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1274 # ------------------------------------------------------------------
1275 # if the copy is not in a state that should persist,
1276 # set the copy to reshelving if it's not already there
1277 # ------------------------------------------------------------------
1278 my ($c, $e) = _reshelve_copy($ctx);
1280 $change = $c unless $change;
1284 $evt = OpenILS::Event->new('NO_CHANGE');
1285 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id)
1286 if( $copy->status == $U->copy_status_from_name('on holds shelf')->id );
1290 $U->commit_db_session($session);
1293 $logger->activity("checkin by user ".$requestor->id." on item ".
1294 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1296 return _checkin_flesh_event($ctx, $evt);
1299 sub _reshelve_copy {
1302 my $copy = $ctx->{copy};
1303 my $reqr = $ctx->{requestor};
1304 my $session = $ctx->{session};
1306 my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1308 if($stat != $U->copy_status_from_name('on holds shelf')->id and
1309 $stat != $U->copy_status_from_name('available')->id and
1310 $stat != $U->copy_status_from_name('cataloging')->id and
1311 $stat != $U->copy_status_from_name('in transit')->id and
1312 $stat != $U->copy_status_from_name('reshelving')->id ) {
1314 $copy->status( $U->copy_status_from_name('reshelving')->id );
1316 my $evt = $U->update_copy(
1318 editor => $reqr->id,
1319 session => $session,
1330 # returns undef if there are no 'open' claims-returned circs attached
1331 # to the given copy. if there is an open claims-returned circ,
1332 # then we check for override mode. if in override, mark the claims-returned
1333 # circ as checked in. if not, return event.
1334 sub _handle_claims_returned {
1336 my $copy = $ctx->{copy};
1338 my $CR = _fetch_open_claims_returned($copy->id);
1339 return undef unless $CR;
1341 # - If the caller has set the override flag, we will check the item in
1342 if($ctx->{override}) {
1344 $CR->checkin_time('now');
1345 $CR->checkin_lib($ctx->{requestor}->ws_ou);
1346 $CR->checkin_staff($ctx->{requestor}->id);
1348 my $stat = $U->storagereq(
1349 'open-ils.storage.direct.action.circulation.update', $CR);
1350 return $U->DB_UPDATE_FAILED($CR) unless $stat;
1351 return OpenILS::Event->new('SUCCESS');
1354 # - if not in override mode, return the CR event
1355 return OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1360 sub _fetch_open_claims_returned {
1362 my $trans = $U->storagereq(
1363 'open-ils.storage.direct.action.circulation.search_where',
1365 target_copy => $copyid,
1366 stop_fines => 'CLAIMSRETURNED',
1367 checkin_time => undef,
1370 return $$trans[0] if $trans && $$trans[0];
1374 # - if the copy is has the 'in process' status, set it to reshelving
1375 #sub _check_in_process {
1378 #my $copy = $ctx->{copy};
1379 #my $reqr = $ctx->{requestor};
1380 #my $ses = $ctx->{session};
1382 #my $stat = $U->copy_status_from_name('in process');
1383 #my $rstat = $U->copy_status_from_name('reshelving');
1385 #if( $stat->id == $copy->status->id ) {
1386 #$logger->info("marking 'in-process' copy ".$copy->id." as 'reshelving'");
1387 #$copy->status( $rstat->id );
1388 #my $evt = $U->update_copy(
1390 #editor => $reqr->id,
1393 #return $evt if $evt;
1395 #$copy->status( $rstat ); # - reflesh the copy status
1401 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1402 sub _checkin_handle_precat {
1405 my $copy = $ctx->{copy};
1410 my $catstat = $U->copy_status_from_name('cataloging');
1412 if( $ctx->{precat} ) {
1414 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1416 if( $copy->status != $catstat->id ) {
1417 $copy->status($catstat->id);
1419 return (undef, 0, $errevt) if (
1420 $errevt = $U->update_copy(
1422 editor => $ctx->{requestor}->id,
1423 session => $ctx->{session} ));
1429 return ($evt, $change, undef);
1433 # returns the appropriate event for the given copy status
1434 # if the copy is not in a 'special' status, undef is returned
1435 sub _checkin_check_copy_status {
1437 my $copy = $ctx->{copy};
1438 my $reqr = $ctx->{requestor};
1439 my $ses = $ctx->{session};
1445 my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1448 if( $status == $U->copy_status_from_name('available')->id ||
1449 $status == $U->copy_status_from_name('checked out')->id ||
1450 $status == $U->copy_status_from_name('in process')->id ||
1451 $status == $U->copy_status_from_name('in transit')->id ||
1452 $status == $U->copy_status_from_name('reshelving')->id );
1454 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1455 if( $status == $U->copy_status_from_name('lost')->id );
1457 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1458 if( $status == $U->copy_status_from_name('missing')->id );
1460 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1464 # my $rstat = $U->copy_status_from_name('reshelving');
1465 # my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1467 # if( $stat == $U->copy_status_from_name('lost')->id ) {
1469 # $evt = OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy );
1471 # } elsif( $stat == $U->copy_status_from_name('missing')->id) {
1473 # $evt = OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy );
1476 # return (undef,$evt) if(!$ctx->{override});
1478 # # we're are now going to attempt to override the failure
1479 # # and set the copy to reshelving
1481 # my $copyid = $copy->id;
1482 # my $userid = $reqr->id;
1485 # # - make sure we have permission
1486 # $e = $U->check_perms( $reqr->id,
1487 # $copy->circ_lib, 'COPY_STATUS_LOST.override');
1488 # return (undef,$e) if $e;
1489 # $copy->status( $rstat->id );
1491 # # XXX if no fines are owed in the circ, close it out - will this happen later anyway?
1492 # #my $circ = $U->storagereq(
1493 # # 'open-ils.storage.direct.action.circulation
1495 # $logger->activity("user $userid overriding 'lost' copy status for copy $copyid");
1497 # } elsif( $ismissing ) {
1499 # # - make sure we have permission
1500 # $e = $U->check_perms( $reqr->id,
1501 # $copy->circ_lib, 'COPY_STATUS_MISSING.override');
1502 # return (undef,$e) if $e;
1503 # $copy->status( $rstat->id );
1504 # $logger->activity("user $userid overriding 'missing' copy status for copy $copyid");
1507 # if( $islost or $ismissing ) {
1509 # # - update the copy with the new status
1510 # $evt = $U->update_copy(
1512 # editor => $reqr->id,
1515 # return (undef,$evt) if $evt;
1516 # $copy->status( $rstat );
1524 # Just gets the copy back home. Returns undef on success, event on error
1525 sub _checkin_build_generic_copy_transit {
1528 my $requestor = $ctx->{requestor};
1529 my $copy = $ctx->{copy};
1530 my $transit = Fieldmapper::action::transit_copy->new;
1531 my $session = $ctx->{session};
1533 $logger->activity("User ". $requestor->id ." creating a ".
1534 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1536 $transit->source($requestor->ws_ou);
1537 $transit->dest($copy->circ_lib);
1538 $transit->target_copy($copy->id);
1539 $transit->source_send_time('now');
1540 $transit->copy_status($copy->status);
1542 $logger->debug("Creating new copy_transit in DB");
1544 my $s = $session->request(
1545 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1546 return $U->DB_UPDATE_FAILED($transit) unless $s;
1548 $logger->info("Checkin copy successfully created new transit: $s");
1550 $copy->status($U->copy_status_from_name('in transit')->id );
1552 return $U->update_copy( copy => $copy,
1553 editor => $requestor->id, session => $session );
1558 # returns event on error, undef on success
1559 sub _checkin_build_hold_transit {
1562 my $copy = $ctx->{copy};
1563 my $hold = $ctx->{hold};
1564 my $trans = Fieldmapper::action::hold_transit_copy->new;
1566 $trans->hold($hold->id);
1567 $trans->source($ctx->{requestor}->ws_ou);
1568 $trans->dest($hold->pickup_lib);
1569 $trans->source_send_time("now");
1570 $trans->target_copy($copy->id);
1571 $trans->copy_status($copy->status);
1573 my $id = $ctx->{session}->request(
1574 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1575 return $U->DB_UPDATE_FAILED($trans) unless $id;
1577 $logger->info("Checkin copy successfully created hold transit: $id");
1579 $copy->status($U->copy_status_from_name('in transit')->id );
1580 return $U->update_copy( copy => $copy,
1581 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1584 # Returns event on error, undef on success
1585 sub _checkin_capture_hold {
1587 my $copy = $ctx->{copy};
1588 my $hold = $ctx->{hold};
1590 $logger->debug("Checkin copy capturing hold ".$hold->id);
1592 $hold->current_copy($copy->id);
1593 $hold->capture_time('now');
1595 my $stat = $ctx->{session}->request(
1596 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1597 return $U->DB_UPDATE_FAILED($hold) unless $stat;
1599 $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1601 return $U->update_copy( copy => $copy,
1602 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1605 # fleshes an event with the relevant objects from the context
1606 sub _checkin_flesh_event {
1611 $payload->{copy} = $U->unflesh_copy($ctx->{copy});
1612 $payload->{record} = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1613 $payload->{circ} = $ctx->{circ} if $ctx->{circ};
1614 $payload->{transit} = $ctx->{transit} if $ctx->{transit};
1615 $payload->{hold} = $ctx->{hold} if $ctx->{hold};
1617 $evt->{payload} = $payload;
1622 # Closes out the circulation, puts the copy into reshelving.
1623 # Voids any bills attached to this circ after the backdate time
1624 # if a backdate is provided
1625 sub _checkin_handle_circ {
1629 my $circ = $ctx->{circ};
1630 my $copy = $ctx->{copy};
1631 my $requestor = $ctx->{requestor};
1632 my $session = $ctx->{session};
1636 $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1638 #$ctx->{longoverdue} = 1 if ($circ->stop_fines =~ /longoverdue/io);
1639 #$ctx->{claimsreturned} = 1 if ($circ->stop_fines =~ /claimsreturned/io);
1641 # backdate the circ if necessary
1642 if(my $backdate = $ctx->{backdate}) {
1643 return $evt if ($evt =
1644 _checkin_handle_backdate($backdate, $circ, $requestor, $session, 1));
1648 if(!$circ->stop_fines) {
1649 $circ->stop_fines('CHECKIN');
1650 $circ->stop_fines('RENEW') if $__isrenewal;
1651 $circ->stop_fines_time('now');
1654 # see if there are any fines owed on this circ. if not, close it
1655 ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1656 return $evt if $evt;
1657 $circ->xact_finish('now') if( $obt->balance_owed <= 0 );
1659 # Set the checkin vars since we have the item
1660 $circ->checkin_time('now');
1661 $circ->checkin_staff($requestor->id);
1662 $circ->checkin_lib($requestor->ws_ou);
1665 # $copy->status($U->copy_status_from_name('reshelving')->id);
1666 # $evt = $U->update_copy( session => $session,
1667 # copy => $copy, editor => $requestor->id );
1668 # return $evt if $evt;
1670 $ctx->{session}->request(
1671 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1676 sub _set_copy_reshelving {
1677 my( $copy, $reqr, $session ) = @_;
1679 $logger->info("Setting copy ".$copy->id." to reshelving");
1680 $copy->status($U->copy_status_from_name('reshelving')->id);
1682 my $evt = $U->update_copy(
1683 session => $session,
1687 return $evt if $evt;
1690 # returns event on error, undef on success
1691 # This voids all bills attached to the given circulation that occurred
1692 # after the backdate
1693 # THIS DOES NOT CLOSE THE CIRC if there are no more fines on the item
1694 sub _checkin_handle_backdate {
1695 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1697 $logger->activity("User ".$requestor->id.
1698 " backdating circ [".$circ->target_copy."] to date: $backdate");
1700 my $bills = $session->request( # XXX Verify this call is correct
1701 "open-ils.storage.direct.money.billing.search_where.atomic",
1702 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1705 for my $bill (@$bills) {
1707 my $s = $session->request(
1708 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1709 return $U->DB_UPDATE_FAILED($bill) unless $s;
1713 # if the caller elects to attempt to close the circulation
1714 # transaction, then it will be closed if there are not further
1715 # charges on the transaction
1717 #my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1718 #return $evt if $evt;
1719 #$circ->xact_finish($backdate) if $obt->balance_owed <= 0;
1726 sub _find_patron_from_params {
1734 if(my $barcode = $params->{barcode}) {
1735 $logger->debug("circ finding user from params with barcode $barcode");
1736 ($copy, $evt) = $U->fetch_copy_by_barcode($barcode);
1737 return (undef, undef, $evt) if $evt;
1738 ($circ, $evt) = $U->fetch_open_circulation($copy->id);
1739 return (undef, undef, $evt) if $evt;
1740 ($patron, $evt) = $U->fetch_user($circ->usr);
1741 return (undef, undef, $evt) if $evt;
1743 return ($patron, $copy);
1747 # ------------------------------------------------------------------------------
1749 __PACKAGE__->register_method(
1751 api_name => "open-ils.circ.renew.override",
1752 signature => q/@see open-ils.circ.renew/,
1756 __PACKAGE__->register_method(
1758 api_name => "open-ils.circ.renew",
1759 notes => <<" NOTES");
1760 PARAMS( authtoken, circ => circ_id );
1761 open-ils.circ.renew(login_session, circ_object);
1762 Renews the provided circulation. login_session is the requestor of the
1763 renewal and if the logged in user is not the same as circ->usr, then
1764 the logged in user must have RENEW_CIRC permissions.
1768 my( $self, $client, $authtoken, $params ) = @_;
1771 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1774 $params->{override} = 1 if $self->api_name =~ /override/o;
1776 # fetch the patron object one way or another
1777 if( $params->{patron} ) {
1778 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1779 if($evt) { $__isrenewal = 0; return $evt; }
1781 } elsif( $params->{patron_barcode} ) {
1782 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1783 if($evt) { $__isrenewal = 0; return $evt; }
1786 ($patron, $copy, $evt) = _find_patron_from_params($params);
1787 return $evt if $evt;
1788 $params->{copy} = $copy;
1791 # verify our login session
1792 ($requestor, $evt) = $U->checkses($authtoken);
1793 if($evt) { $__isrenewal = 0; return $evt; }
1795 # make sure we have permission to perform a renewal
1796 if( $requestor->id ne $patron->id ) {
1797 $evt = $U->check_perms($requestor->id, $requestor->ws_ou, 'RENEW_CIRC');
1798 if($evt) { $__isrenewal = 0; return $evt; }
1802 # fetch and build the circulation environment
1803 ( $ctx, $evt ) = create_circ_ctx( %$params,
1805 requestor => $requestor,
1808 #fetch_patron_circ_summary => 1,
1809 fetch_copy_statuses => 1,
1810 fetch_copy_locations => 1,
1812 if($evt) { $__isrenewal = 0; return $evt; }
1813 $params->{_ctx} = $ctx;
1815 # make sure they have some renewals left and make sure the circulation exists
1816 ($circ, $evt) = _check_renewal_remaining($ctx);
1817 if($evt) { $__isrenewal = 0; return $evt; }
1818 $ctx->{old_circ} = $circ;
1819 my $renewals = $circ->renewal_remaining - 1;
1821 # run the renew permit script
1822 $evt = _run_renew_scripts($ctx);
1823 if($evt) { $__isrenewal = 0; return $evt; }
1826 #$ctx->{patron} = $ctx->{patron}->id;
1827 $evt = $self->generic_receive($client, $authtoken, $ctx );
1828 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1830 if( !$U->event_equals($evt, 'SUCCESS') ) {
1831 $__isrenewal = 0; return $evt;
1834 # re-fetch the context since objects have changed in the checkin
1835 ( $ctx, $evt ) = create_circ_ctx( %$params,
1837 requestor => $requestor,
1840 #fetch_patron_circ_summary => 1,
1841 fetch_copy_statuses => 1,
1842 fetch_copy_locations => 1,
1844 if($evt) { $__isrenewal = 0; return $evt; }
1845 $params->{_ctx} = $ctx;
1846 $ctx->{renewal_remaining} = $renewals;
1848 # run the circ permit scripts
1849 if( $ctx->{permit_override} ) {
1850 $evt = $U->check_perms(
1851 $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1852 if($evt) { $__isrenewal = 0; return $evt; }
1855 $evt = $self->permit_circ( $client, $authtoken, $params );
1856 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1857 #$ctx->{precat} = 1;
1858 $params->{precat} = 1;
1861 if(!$U->event_equals($evt, 'SUCCESS')) {
1862 if($evt) { $__isrenewal = 0; return $evt; }
1865 $params->{permit_key} = $evt->{payload};
1869 # checkout the item again
1870 $params->{patron} = $ctx->{patron}->id;
1871 $evt = $self->checkout($client, $authtoken, $params );
1873 $logger->activity("user ".$requestor->id." renewl of item ".
1874 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1880 sub _check_renewal_remaining {
1883 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1884 return (undef, $evt) if $evt;
1885 $evt = OpenILS::Event->new(
1886 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1887 return ($circ, $evt);
1890 sub _run_renew_scripts {
1892 my $runner = $ctx->{runner};
1895 $runner->load($scripts{circ_permit_renew});
1896 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1898 my $events = $runner->retrieve('result.events');
1899 $events = [ split(/,/, $events) ];
1900 $logger->activity("circ_permit_renew for user ".
1901 $ctx->{patron}->id." returned events: @$events") if @$events;
1904 push( @allevents, OpenILS::Event->new($_)) for @$events;
1905 return \@allevents if @allevents;