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 'ASSET_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 # XXX add pickup lib to the call to test for perms
477 sub check_title_hold {
478 my( $self, $client, $authtoken, $params ) = @_;
479 my %params = %$params;
480 my $titleid = $params{titleid};
482 my ( $requestor, $patron, $evt ) = $U->checkses_requestor(
483 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
486 my $rangelib = $patron->home_ou;
487 my $depth = $params{depth} || 0;
489 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
491 my $org = $U->simplereq(
493 'open-ils.actor.org_unit.retrieve',
494 $authtoken, $requestor->home_ou );
500 while( $title = $U->storagereq(
501 'open-ils.storage.biblio.record_entry.ranged_tree',
502 $titleid, $rangelib, $depth, $limit, $offset ) ) {
504 last unless ref($title);
506 for my $cn (@{$title->call_numbers}) {
508 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
510 for my $copy (@{$cn->copies}) {
512 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
514 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
516 requestor => $requestor,
519 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
520 request_lib => $org } );
522 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
533 # Runs the patron and copy permit scripts
534 # if this is a non-cat circulation, the copy permit script
536 sub _run_permit_scripts {
539 my $runner = $ctx->{runner};
540 my $patronid = $ctx->{patron}->id;
541 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
542 my $key = $ctx->{permit_key};
544 my $penalties = $U->update_patron_penalties(
545 authtoken => $ctx->{authtoken},
546 patron => $ctx->{patron}
549 $penalties = $penalties->{fatal_penalties};
551 $logger->info("circ patron penalties user $patronid: @$penalties");
553 if( $ctx->{noncat} ) {
554 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
555 return OpenILS::Event->new('SUCCESS', payload => $key);
559 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
560 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
564 $logger->debug("Exiting circ permit early because request is for hold patron permit");
565 return OpenILS::Event->new('SUCCESS');
568 $runner->load($scripts{circ_permit_copy});
569 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
571 # ---------------------------------------------------------------------
572 # Capture all of the copy permit events
573 # ---------------------------------------------------------------------
574 my $copy_events = $runner->retrieve('result.events');
575 $copy_events = [ split(/,/, $copy_events) ];
576 $ctx->{circ_permit_copy_events} = $copy_events;
577 $logger->activity("circ_permit_copy for copy ".
578 "$barcode returned events: @$copy_events") if @$copy_events;
581 push( @allevents, OpenILS::Event->new($_)) for @$penalties;
582 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
584 my $ae = _check_copy_alert($ctx->{copy});
585 push( @allevents, $ae ) if $ae;
587 return OpenILS::Event->new('SUCCESS', payload => $key) unless (@allevents);
589 # uniquify the events
590 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
591 @allevents = values %hash;
594 $_->{payload} = $ctx->{copy}->status->id
595 if ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
601 sub _check_copy_alert {
603 return OpenILS::Event->new('COPY_ALERT_MESSAGE',
604 payload => $copy->alert_message) if $copy->alert_message;
608 # takes copyid, patronid, and requestor id
609 sub _cache_permit_key {
610 my $key = md5_hex( time() . rand() . "$$" );
611 $logger->debug("Setting circ permit key to $key");
612 $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
616 sub _check_permit_key {
618 $logger->debug("Fetching circ permit key $key");
619 my $k = "oils_permit_key_$key";
620 my $one = $cache_handle->get_cache($k);
621 $cache_handle->delete_cache($k);
622 return ($one) ? 1 : 0;
626 # ------------------------------------------------------------------------------
628 __PACKAGE__->register_method(
629 method => "checkout",
630 api_name => "open-ils.circ.checkout",
633 @param authtoken The login session key
634 @param params A named hash of params including:
636 barcode If no copy is provided, the copy is retrieved via barcode
637 copyid If no copy or barcode is provide, the copy id will be use
638 patron The patron's id
639 noncat True if this is a circulation for a non-cataloted item
640 noncat_type The non-cataloged type id
641 noncat_circ_lib The location for the noncat circ.
642 precat The item has yet to be cataloged
643 dummy_title The temporary title of the pre-cataloded item
644 dummy_author The temporary authr of the pre-cataloded item
645 Default is the home org of the staff member
646 @return The SUCCESS event on success, any other event depending on the error
650 my( $self, $client, $authtoken, $params ) = @_;
653 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
654 my $key = $params->{permit_key};
656 # if this is a renewal, then the requestor does not have to
657 # have checkout privelages
658 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
659 ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
662 if( $params->{patron} ) {
663 ( $patron, $evt ) = $U->fetch_user($params->{patron});
666 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
670 # set the circ lib to the home org of the requestor if not specified
671 my $circlib = (defined($params->{circ_lib})) ?
672 $params->{circ_lib} : $requestor->ws_ou;
675 # Make sure the caller has a valid permit key or is
676 # overriding the permit can
677 if( $params->{permit_override} ) {
678 $evt = $U->check_perms(
679 $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
683 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
684 unless _check_permit_key($key);
687 # if this is a non-cataloged item, check it out and return
688 return _checkout_noncat(
689 $key, $requestor, $patron, %$params ) if $params->{noncat};
691 # if this item has yet to be cataloged, make sure a dummy copy exists
692 ( $params->{copy}, $evt ) = _make_precat_copy(
693 $requestor, $circlib, $params ) if $params->{precat};
697 # fetch and build the circulation environment
698 if( !( $ctx = $params->{_ctx}) ) {
699 ( $ctx, $evt ) = create_circ_ctx( %$params,
701 requestor => $requestor,
702 session => $U->start_db_session(),
704 #fetch_patron_circ_summary => 1,
705 fetch_copy_statuses => 1,
706 fetch_copy_locations => 1,
710 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
712 # if the call doesn't know it's not cataloged..
713 if(!$params->{precat}) {
714 if( $ctx->{copy}->call_number eq '-1' ) {
715 return OpenILS::Event->new('ITEM_NOT_CATALOGED');
719 # this happens in permit.. but we need to check here for 'offline' requests
720 ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
721 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
723 my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
726 $ctx->{circ_lib} = $circlib;
728 $evt = _run_checkout_scripts($ctx);
732 _build_checkout_circ_object($ctx);
734 $evt = _apply_modified_due_date($ctx);
737 $evt = _commit_checkout_circ_object($ctx);
740 $evt = _update_checkout_copy($ctx);
744 ($holds, $evt) = _handle_related_holds($ctx);
748 $logger->debug("Checkout committing objects with session thread trace: ".$ctx->{session}->session_id);
749 $U->commit_db_session($ctx->{session});
750 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
752 $logger->activity("user ".$requestor->id." successfully checked out item ".
753 $ctx->{copy}->barcode." to user ".$ctx->{patron}->id );
756 # ------------------------------------------------------------------------------
757 # Update the patron penalty info in the DB
758 # ------------------------------------------------------------------------------
759 $U->update_patron_penalties(
760 authtoken => $authtoken,
761 patron => $ctx->{patron} ,
765 return OpenILS::Event->new('SUCCESS',
767 copy => $U->unflesh_copy($ctx->{copy}),
768 circ => $ctx->{circ},
770 holds_fulfilled => $holds,
776 sub _make_precat_copy {
777 my ( $requestor, $circlib, $params ) = @_;
779 my( $copy, undef ) = _find_copy_by_attr(%$params);
782 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
784 $copy->editor($requestor->id);
785 $copy->edit_date('now');
786 $copy->dummy_title($params->{dummy_title});
787 $copy->dummy_author($params->{dummy_author});
789 my $stat = $U->storagereq(
790 'open-ils.storage.direct.asset.copy.update', $copy );
792 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $stat;
796 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
798 my $evt = OpenILS::Event->new(
799 'BAD_PARAMS', desc => "Dummy title or author not provided" )
800 unless ( $params->{dummy_title} and $params->{dummy_author} );
801 return (undef, $evt) if $evt;
803 $copy = Fieldmapper::asset::copy->new;
804 $copy->circ_lib($circlib);
805 $copy->creator($requestor->id);
806 $copy->editor($requestor->id);
807 $copy->barcode($params->{barcode});
808 $copy->call_number(-1); #special CN for precat materials
809 $copy->loan_duration(&PRECAT_LOAN_DURATION);
810 $copy->fine_level(&PRECAT_FINE_LEVEL);
812 $copy->dummy_title($params->{dummy_title});
813 $copy->dummy_author($params->{dummy_author});
815 my $id = $U->storagereq(
816 'open-ils.storage.direct.asset.copy.create', $copy );
817 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
819 $logger->debug("Pre-cataloged copy successfully created");
820 return ($U->fetch_copy($id));
824 sub _run_checkout_scripts {
830 my $runner = $ctx->{runner};
832 $runner->insert('result.durationLevel');
833 $runner->insert('result.durationRule');
834 $runner->insert('result.recurringFinesRule');
835 $runner->insert('result.recurringFinesLevel');
836 $runner->insert('result.maxFine');
838 $runner->load($scripts{circ_duration});
839 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
840 my $duration = $runner->retrieve('result.durationRule');
841 $logger->debug("Circ duration script yielded a duration rule of: $duration");
843 $runner->load($scripts{circ_recurring_fines});
844 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
845 my $recurring = $runner->retrieve('result.recurringFinesRule');
846 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
848 $runner->load($scripts{circ_max_fines});
849 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
850 my $max_fine = $runner->retrieve('result.maxFine');
851 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
853 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
855 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
857 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
860 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
861 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
862 $ctx->{duration_rule} = $duration;
863 $ctx->{recurring_fines_rule} = $recurring;
864 $ctx->{max_fine_rule} = $max_fine;
869 sub _build_checkout_circ_object {
873 my $circ = new Fieldmapper::action::circulation;
874 my $duration = $ctx->{duration_rule};
875 my $max = $ctx->{max_fine_rule};
876 my $recurring = $ctx->{recurring_fines_rule};
877 my $copy = $ctx->{copy};
878 my $patron = $ctx->{patron};
879 my $dur_level = $ctx->{duration_level};
880 my $rec_level = $ctx->{recurring_fines_level};
882 $circ->duration( $duration->shrt ) if ($dur_level == 1);
883 $circ->duration( $duration->normal ) if ($dur_level == 2);
884 $circ->duration( $duration->extended ) if ($dur_level == 3);
886 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
887 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
888 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
890 $circ->duration_rule( $duration->name );
891 $circ->recuring_fine_rule( $recurring->name );
892 $circ->max_fine_rule( $max->name );
893 $circ->max_fine( $max->amount );
895 $circ->fine_interval($recurring->recurance_interval);
896 $circ->renewal_remaining( $duration->max_renewals );
897 $circ->target_copy( $copy->id );
898 $circ->usr( $patron->id );
899 $circ->circ_lib( $ctx->{circ_lib} );
902 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
903 $circ->opac_renewal(1);
904 $circ->renewal_remaining($ctx->{renewal_remaining});
905 $circ->circ_staff($ctx->{requestor}->id);
909 # if the user provided an overiding checkout time,
910 # (e.g. the checkout really happened several hours ago), then
911 # we apply that here. Does this need a perm??
912 if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
913 $logger->debug("circ setting checkout_time to $ds");
914 $circ->xact_start($ds);
917 # if a patron is renewing, 'requestor' will be the patron
918 $circ->circ_staff($ctx->{requestor}->id );
919 _set_circ_due_date($circ);
920 $ctx->{circ} = $circ;
923 sub _apply_modified_due_date {
925 my $circ = $ctx->{circ};
927 if( $ctx->{due_date} ) {
929 my $evt = $U->check_perms(
930 $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
933 my $ds = _create_date_stamp($ctx->{due_date});
934 $logger->debug("circ modifying due_date to $ds");
935 $circ->due_date($ds);
941 sub _create_date_stamp {
942 my $datestring = shift;
943 return undef unless $datestring;
944 $datestring = clense_ISO8601($datestring);
945 $logger->debug("circ created date stamp => $datestring");
949 sub _create_due_date {
950 my $duration = shift;
952 my ($sec,$min,$hour,$mday,$mon,$year) =
953 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
954 $year += 1900; $mon += 1;
955 my $due_date = sprintf(
956 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
957 $year, $mon, $mday, $hour, $min, $sec);
961 sub _set_circ_due_date {
964 my $dd = _create_due_date($circ->duration);
965 $logger->debug("Checkout setting due date on circ to: $dd");
966 $circ->due_date($dd);
969 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
970 sub _update_checkout_copy {
973 my $copy = $ctx->{copy};
975 my $s = $U->copy_status_from_name('checked out');
976 $copy->status( $s->id ) if $s;
978 my $evt = $U->update_copy( session => $ctx->{session},
979 copy => $copy, editor => $ctx->{requestor}->id );
980 return (undef,$evt) if $evt;
985 # commits the circ object to the db then fleshes the circ with rules objects
986 sub _commit_checkout_circ_object {
989 my $circ = $ctx->{circ};
993 my $r = $ctx->{session}->request(
994 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
996 return $U->DB_UPDATE_FAILED($circ) unless $r;
998 $logger->debug("Created a new circ object in checkout: $r");
1001 $circ->duration_rule($ctx->{duration_rule});
1002 $circ->max_fine_rule($ctx->{max_fine_rule});
1003 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
1009 # sees if there are any holds that this copy
1010 sub _handle_related_holds {
1013 my $copy = $ctx->{copy};
1014 my $patron = $ctx->{patron};
1015 my $holds = $holdcode->fetch_related_holds($copy->id);
1019 # XXX We should only fulfill one hold here...
1020 # XXX If a hold was transited to the user who is checking out
1021 # the item, we need to make sure that hold is what's grabbed
1022 if(ref($holds) && @$holds) {
1024 # for now, just sort by id to get what should be the oldest hold
1025 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1026 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
1029 my $hold = $holds->[0];
1031 $logger->debug("Related hold found in checkout: " . $hold->id );
1033 $hold->current_copy($copy->id); # just make sure it's set
1034 # if the hold was never officially captured, capture it.
1035 $hold->capture_time('now') unless $hold->capture_time;
1036 $hold->fulfillment_time('now');
1037 my $r = $ctx->{session}->request(
1038 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
1039 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
1040 push( @fulfilled, $hold->id );
1044 return (\@fulfilled, undef);
1047 sub _checkout_noncat {
1048 my ( $key, $requestor, $patron, %params ) = @_;
1049 my( $circ, $circlib, $evt );
1052 $circlib = $params{noncat_circ_lib} || $requestor->ws_ou;
1054 my $count = $params{noncat_count} || 1;
1055 my $cotime = _create_date_stamp($params{checkout_time}) || "";
1056 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1058 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1059 $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
1060 return $evt if $evt;
1063 return OpenILS::Event->new(
1064 'SUCCESS', payload => { noncat_circ => $circ } );
1068 __PACKAGE__->register_method(
1069 method => "generic_receive",
1070 api_name => "open-ils.circ.checkin",
1073 Generic super-method for handling all copies
1074 @param authtoken The login session key
1075 @param params Hash of named parameters including:
1076 barcode - The copy barcode
1077 force - If true, copies in bad statuses will be checked in and give good statuses
1082 __PACKAGE__->register_method(
1083 method => "generic_receive",
1084 api_name => "open-ils.circ.checkin.override",
1085 signature => q/@see open-ils.circ.checkin/
1088 sub generic_receive {
1089 my( $self, $conn, $authtoken, $params ) = @_;
1090 my( $ctx, $requestor, $evt );
1092 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1093 ( $requestor, $evt ) = $U->checksesperm(
1094 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1095 return $evt if $evt;
1097 # load up the circ objects
1098 if( !( $ctx = $params->{_ctx}) ) {
1099 ( $ctx, $evt ) = create_circ_ctx( %$params,
1100 requestor => $requestor,
1101 session => $U->start_db_session(),
1103 fetch_copy_statuses => 1,
1104 fetch_copy_locations => 1,
1107 return $evt if $evt;
1109 $ctx->{override} = 1 if $self->api_name =~ /override/o;
1110 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1111 $ctx->{authtoken} = $authtoken;
1112 my $session = $ctx->{session};
1114 my $copy = $ctx->{copy};
1115 $U->unflesh_copy($copy);
1116 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $copy;
1118 $logger->info("Checkin copy called by user ".
1119 $requestor->id." for copy ".$copy->id);
1121 # ------------------------------------------------------------------------------
1122 # Update the patron penalty info in the DB
1123 # ------------------------------------------------------------------------------
1124 $U->update_patron_penalties(
1125 authtoken => $authtoken,
1126 patron => $ctx->{patron},
1130 return $self->checkin_do_receive($conn, $ctx);
1133 sub checkin_do_receive {
1135 my( $self, $connection, $ctx ) = @_;
1138 my $copy = $ctx->{copy};
1139 my $session = $ctx->{session};
1140 my $requestor = $ctx->{requestor};
1141 my $change = 0; # did we actually do anything?
1146 # does the copy have an attached alert message?
1147 my $ae = _check_copy_alert($copy);
1148 push(@eventlist, $ae) if $ae;
1150 # is the copy is an a status we can't automatically resolve?
1151 $evt = _checkin_check_copy_status($ctx);
1152 push( @eventlist, $evt ) if $evt;
1155 # - see if the copy has an open circ attached
1156 ($ctx->{circ}, $evt) = $U->fetch_open_circulation($copy->id);
1157 return $evt if ($evt and $__isrenewal); # renewals require a circulation
1159 $circ = $ctx->{circ};
1161 # if the circ is marked as 'claims returned', add the event to the list
1162 push( @eventlist, 'CIRC_CLAIMS_RETURNED' )
1163 if ($circ and $circ->stop_fines eq 'CLAIMSRETURNED');
1167 if($ctx->{override}) {
1168 $evt = override_events($requestor, $requestor->ws_ou, \@eventlist );
1169 return $evt if $evt;
1175 ($ctx->{transit}) = $U->fetch_open_transit_by_copy($copy->id);
1177 if( $ctx->{circ} ) {
1179 # There is an open circ on this item, close it out.
1181 $evt = _checkin_handle_circ($ctx);
1182 return $evt if $evt;
1184 } elsif( $ctx->{transit} ) {
1186 # is this item currently in transit?
1188 $evt = $transcode->transit_receive( $copy, $requestor, $session );
1189 my $holdtrans = $evt->{holdtransit};
1190 ($ctx->{hold}) = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1192 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1194 # either an error occurred or a ROUTE_ITEM was generated and the
1195 # item must be forwarded on to its destination.
1196 return _checkin_flesh_event($ctx, $evt);
1200 # Transit has been closed, now let's see if the copy's original
1201 # status is something the staff should be warned of
1202 my $e = _checkin_check_copy_status($ctx);
1207 # copy was received as a hold transit. Copy is at target lib
1208 # and hold transit is complete. We're done here...
1209 $U->commit_db_session($session);
1210 return _checkin_flesh_event($ctx, $evt);
1216 # ------------------------------------------------------------------------------
1217 # Circulations and transits are now closed where necessary. Now go on to see if
1218 # this copy can fulfill a hold or needs to be routed to a different location
1219 # ------------------------------------------------------------------------------
1222 # If it's a renewal, we're done
1224 $U->commit_db_session($session);
1225 return OpenILS::Event->new('SUCCESS');
1228 # Now, let's see if this copy is needed for a hold
1229 my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor );
1233 $ctx->{hold} = $hold;
1236 # Capture the hold with this copy
1237 return $evt if ($evt = _checkin_capture_hold($ctx));
1239 if( $hold->pickup_lib == $requestor->ws_ou ) {
1241 # This hold was captured in the correct location
1242 $evt = OpenILS::Event->new('SUCCESS');
1246 # Hold needs to be picked up elsewhere. Build a hold
1247 # transit and route the item.
1248 return $evt if ($evt =_checkin_build_hold_transit($ctx));
1249 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1252 } else { # not needed for a hold
1254 if( $copy->circ_lib == $requestor->ws_ou ) {
1256 # Copy is in the right place.
1257 $evt = OpenILS::Event->new('SUCCESS');
1259 # if the item happens to be a pre-cataloged item, send it
1260 # to cataloging and return the event
1261 my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1262 return $err if $err;
1268 # Copy wants to go home. Transit it there.
1269 return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1270 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1276 # ------------------------------------------------------------------
1277 # if the copy is not in a state that should persist,
1278 # set the copy to reshelving if it's not already there
1279 # ------------------------------------------------------------------
1280 my ($c, $e) = _reshelve_copy($ctx);
1282 $change = $c unless $change;
1286 $evt = OpenILS::Event->new('NO_CHANGE');
1287 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id)
1288 if( $copy->status == $U->copy_status_from_name('on holds shelf')->id );
1292 $U->commit_db_session($session);
1295 $logger->activity("checkin by user ".$requestor->id." on item ".
1296 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1298 return _checkin_flesh_event($ctx, $evt);
1301 sub _reshelve_copy {
1304 my $copy = $ctx->{copy};
1305 my $reqr = $ctx->{requestor};
1306 my $session = $ctx->{session};
1308 my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1310 if($stat != $U->copy_status_from_name('on holds shelf')->id and
1311 $stat != $U->copy_status_from_name('available')->id and
1312 $stat != $U->copy_status_from_name('cataloging')->id and
1313 $stat != $U->copy_status_from_name('in transit')->id and
1314 $stat != $U->copy_status_from_name('reshelving')->id ) {
1316 $copy->status( $U->copy_status_from_name('reshelving')->id );
1318 my $evt = $U->update_copy(
1320 editor => $reqr->id,
1321 session => $session,
1332 # returns undef if there are no 'open' claims-returned circs attached
1333 # to the given copy. if there is an open claims-returned circ,
1334 # then we check for override mode. if in override, mark the claims-returned
1335 # circ as checked in. if not, return event.
1336 sub _handle_claims_returned {
1338 my $copy = $ctx->{copy};
1340 my $CR = _fetch_open_claims_returned($copy->id);
1341 return undef unless $CR;
1343 # - If the caller has set the override flag, we will check the item in
1344 if($ctx->{override}) {
1346 $CR->checkin_time('now');
1347 $CR->checkin_lib($ctx->{requestor}->ws_ou);
1348 $CR->checkin_staff($ctx->{requestor}->id);
1350 my $stat = $U->storagereq(
1351 'open-ils.storage.direct.action.circulation.update', $CR);
1352 return $U->DB_UPDATE_FAILED($CR) unless $stat;
1353 return OpenILS::Event->new('SUCCESS');
1356 # - if not in override mode, return the CR event
1357 return OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1362 sub _fetch_open_claims_returned {
1364 my $trans = $U->storagereq(
1365 'open-ils.storage.direct.action.circulation.search_where',
1367 target_copy => $copyid,
1368 stop_fines => 'CLAIMSRETURNED',
1369 checkin_time => undef,
1372 return $$trans[0] if $trans && $$trans[0];
1376 # - if the copy is has the 'in process' status, set it to reshelving
1377 #sub _check_in_process {
1380 #my $copy = $ctx->{copy};
1381 #my $reqr = $ctx->{requestor};
1382 #my $ses = $ctx->{session};
1384 #my $stat = $U->copy_status_from_name('in process');
1385 #my $rstat = $U->copy_status_from_name('reshelving');
1387 #if( $stat->id == $copy->status->id ) {
1388 #$logger->info("marking 'in-process' copy ".$copy->id." as 'reshelving'");
1389 #$copy->status( $rstat->id );
1390 #my $evt = $U->update_copy(
1392 #editor => $reqr->id,
1395 #return $evt if $evt;
1397 #$copy->status( $rstat ); # - reflesh the copy status
1403 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1404 sub _checkin_handle_precat {
1407 my $copy = $ctx->{copy};
1412 my $catstat = $U->copy_status_from_name('cataloging');
1414 if( $ctx->{precat} ) {
1416 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1418 if( $copy->status != $catstat->id ) {
1419 $copy->status($catstat->id);
1421 return (undef, 0, $errevt) if (
1422 $errevt = $U->update_copy(
1424 editor => $ctx->{requestor}->id,
1425 session => $ctx->{session} ));
1431 return ($evt, $change, undef);
1435 # returns the appropriate event for the given copy status
1436 # if the copy is not in a 'special' status, undef is returned
1437 sub _checkin_check_copy_status {
1439 my $copy = $ctx->{copy};
1440 my $reqr = $ctx->{requestor};
1441 my $ses = $ctx->{session};
1447 my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1450 if( $status == $U->copy_status_from_name('available')->id ||
1451 $status == $U->copy_status_from_name('checked out')->id ||
1452 $status == $U->copy_status_from_name('in process')->id ||
1453 $status == $U->copy_status_from_name('in transit')->id ||
1454 $status == $U->copy_status_from_name('reshelving')->id );
1456 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1457 if( $status == $U->copy_status_from_name('lost')->id );
1459 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1460 if( $status == $U->copy_status_from_name('missing')->id );
1462 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1466 # my $rstat = $U->copy_status_from_name('reshelving');
1467 # my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1469 # if( $stat == $U->copy_status_from_name('lost')->id ) {
1471 # $evt = OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy );
1473 # } elsif( $stat == $U->copy_status_from_name('missing')->id) {
1475 # $evt = OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy );
1478 # return (undef,$evt) if(!$ctx->{override});
1480 # # we're are now going to attempt to override the failure
1481 # # and set the copy to reshelving
1483 # my $copyid = $copy->id;
1484 # my $userid = $reqr->id;
1487 # # - make sure we have permission
1488 # $e = $U->check_perms( $reqr->id,
1489 # $copy->circ_lib, 'COPY_STATUS_LOST.override');
1490 # return (undef,$e) if $e;
1491 # $copy->status( $rstat->id );
1493 # # XXX if no fines are owed in the circ, close it out - will this happen later anyway?
1494 # #my $circ = $U->storagereq(
1495 # # 'open-ils.storage.direct.action.circulation
1497 # $logger->activity("user $userid overriding 'lost' copy status for copy $copyid");
1499 # } elsif( $ismissing ) {
1501 # # - make sure we have permission
1502 # $e = $U->check_perms( $reqr->id,
1503 # $copy->circ_lib, 'COPY_STATUS_MISSING.override');
1504 # return (undef,$e) if $e;
1505 # $copy->status( $rstat->id );
1506 # $logger->activity("user $userid overriding 'missing' copy status for copy $copyid");
1509 # if( $islost or $ismissing ) {
1511 # # - update the copy with the new status
1512 # $evt = $U->update_copy(
1514 # editor => $reqr->id,
1517 # return (undef,$evt) if $evt;
1518 # $copy->status( $rstat );
1526 # Just gets the copy back home. Returns undef on success, event on error
1527 sub _checkin_build_generic_copy_transit {
1530 my $requestor = $ctx->{requestor};
1531 my $copy = $ctx->{copy};
1532 my $transit = Fieldmapper::action::transit_copy->new;
1533 my $session = $ctx->{session};
1535 $logger->activity("User ". $requestor->id ." creating a ".
1536 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1538 $transit->source($requestor->ws_ou);
1539 $transit->dest($copy->circ_lib);
1540 $transit->target_copy($copy->id);
1541 $transit->source_send_time('now');
1542 $transit->copy_status($copy->status);
1544 $logger->debug("Creating new copy_transit in DB");
1546 my $s = $session->request(
1547 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1548 return $U->DB_UPDATE_FAILED($transit) unless $s;
1550 $logger->info("Checkin copy successfully created new transit: $s");
1552 $copy->status($U->copy_status_from_name('in transit')->id );
1554 return $U->update_copy( copy => $copy,
1555 editor => $requestor->id, session => $session );
1560 # returns event on error, undef on success
1561 sub _checkin_build_hold_transit {
1564 my $copy = $ctx->{copy};
1565 my $hold = $ctx->{hold};
1566 my $trans = Fieldmapper::action::hold_transit_copy->new;
1568 $trans->hold($hold->id);
1569 $trans->source($ctx->{requestor}->ws_ou);
1570 $trans->dest($hold->pickup_lib);
1571 $trans->source_send_time("now");
1572 $trans->target_copy($copy->id);
1573 $trans->copy_status($copy->status);
1575 my $id = $ctx->{session}->request(
1576 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1577 return $U->DB_UPDATE_FAILED($trans) unless $id;
1579 $logger->info("Checkin copy successfully created hold transit: $id");
1581 $copy->status($U->copy_status_from_name('in transit')->id );
1582 return $U->update_copy( copy => $copy,
1583 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1586 # Returns event on error, undef on success
1587 sub _checkin_capture_hold {
1589 my $copy = $ctx->{copy};
1590 my $hold = $ctx->{hold};
1592 $logger->debug("Checkin copy capturing hold ".$hold->id);
1594 $hold->current_copy($copy->id);
1595 $hold->capture_time('now');
1597 my $stat = $ctx->{session}->request(
1598 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1599 return $U->DB_UPDATE_FAILED($hold) unless $stat;
1601 $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1603 return $U->update_copy( copy => $copy,
1604 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1607 # fleshes an event with the relevant objects from the context
1608 sub _checkin_flesh_event {
1613 $payload->{copy} = $U->unflesh_copy($ctx->{copy});
1614 $payload->{record} = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1615 $payload->{circ} = $ctx->{circ} if $ctx->{circ};
1616 $payload->{transit} = $ctx->{transit} if $ctx->{transit};
1617 $payload->{hold} = $ctx->{hold} if $ctx->{hold};
1619 $evt->{payload} = $payload;
1624 # Closes out the circulation, puts the copy into reshelving.
1625 # Voids any bills attached to this circ after the backdate time
1626 # if a backdate is provided
1627 sub _checkin_handle_circ {
1631 my $circ = $ctx->{circ};
1632 my $copy = $ctx->{copy};
1633 my $requestor = $ctx->{requestor};
1634 my $session = $ctx->{session};
1638 $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1640 #$ctx->{longoverdue} = 1 if ($circ->stop_fines =~ /longoverdue/io);
1641 #$ctx->{claimsreturned} = 1 if ($circ->stop_fines =~ /claimsreturned/io);
1643 # backdate the circ if necessary
1644 if(my $backdate = $ctx->{backdate}) {
1645 return $evt if ($evt =
1646 _checkin_handle_backdate($backdate, $circ, $requestor, $session, 1));
1650 if(!$circ->stop_fines) {
1651 $circ->stop_fines('CHECKIN');
1652 $circ->stop_fines('RENEW') if $__isrenewal;
1653 $circ->stop_fines_time('now');
1656 # see if there are any fines owed on this circ. if not, close it
1657 ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1658 return $evt if $evt;
1659 $circ->xact_finish('now') if( $obt->balance_owed <= 0 );
1661 # Set the checkin vars since we have the item
1662 $circ->checkin_time('now');
1663 $circ->checkin_staff($requestor->id);
1664 $circ->checkin_lib($requestor->ws_ou);
1667 # $copy->status($U->copy_status_from_name('reshelving')->id);
1668 # $evt = $U->update_copy( session => $session,
1669 # copy => $copy, editor => $requestor->id );
1670 # return $evt if $evt;
1672 $ctx->{session}->request(
1673 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1678 sub _set_copy_reshelving {
1679 my( $copy, $reqr, $session ) = @_;
1681 $logger->info("Setting copy ".$copy->id." to reshelving");
1682 $copy->status($U->copy_status_from_name('reshelving')->id);
1684 my $evt = $U->update_copy(
1685 session => $session,
1689 return $evt if $evt;
1692 # returns event on error, undef on success
1693 # This voids all bills attached to the given circulation that occurred
1694 # after the backdate
1695 # THIS DOES NOT CLOSE THE CIRC if there are no more fines on the item
1696 sub _checkin_handle_backdate {
1697 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1699 $logger->activity("User ".$requestor->id.
1700 " backdating circ [".$circ->target_copy."] to date: $backdate");
1702 my $bills = $session->request( # XXX Verify this call is correct
1703 "open-ils.storage.direct.money.billing.search_where.atomic",
1704 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1707 for my $bill (@$bills) {
1709 my $s = $session->request(
1710 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1711 return $U->DB_UPDATE_FAILED($bill) unless $s;
1715 # if the caller elects to attempt to close the circulation
1716 # transaction, then it will be closed if there are not further
1717 # charges on the transaction
1719 #my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1720 #return $evt if $evt;
1721 #$circ->xact_finish($backdate) if $obt->balance_owed <= 0;
1728 sub _find_patron_from_params {
1736 if(my $barcode = $params->{barcode}) {
1737 $logger->debug("circ finding user from params with barcode $barcode");
1738 ($copy, $evt) = $U->fetch_copy_by_barcode($barcode);
1739 return (undef, undef, $evt) if $evt;
1740 ($circ, $evt) = $U->fetch_open_circulation($copy->id);
1741 return (undef, undef, $evt) if $evt;
1742 ($patron, $evt) = $U->fetch_user($circ->usr);
1743 return (undef, undef, $evt) if $evt;
1745 return ($patron, $copy);
1749 # ------------------------------------------------------------------------------
1751 __PACKAGE__->register_method(
1753 api_name => "open-ils.circ.renew.override",
1754 signature => q/@see open-ils.circ.renew/,
1758 __PACKAGE__->register_method(
1760 api_name => "open-ils.circ.renew",
1761 notes => <<" NOTES");
1762 PARAMS( authtoken, circ => circ_id );
1763 open-ils.circ.renew(login_session, circ_object);
1764 Renews the provided circulation. login_session is the requestor of the
1765 renewal and if the logged in user is not the same as circ->usr, then
1766 the logged in user must have RENEW_CIRC permissions.
1770 my( $self, $client, $authtoken, $params ) = @_;
1773 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1776 $params->{override} = 1 if $self->api_name =~ /override/o;
1778 # fetch the patron object one way or another
1779 if( $params->{patron} ) {
1780 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1781 if($evt) { $__isrenewal = 0; return $evt; }
1783 } elsif( $params->{patron_barcode} ) {
1784 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1785 if($evt) { $__isrenewal = 0; return $evt; }
1788 ($patron, $copy, $evt) = _find_patron_from_params($params);
1789 return $evt if $evt;
1790 $params->{copy} = $copy;
1793 # verify our login session
1794 ($requestor, $evt) = $U->checkses($authtoken);
1795 if($evt) { $__isrenewal = 0; return $evt; }
1797 # make sure we have permission to perform a renewal
1798 if( $requestor->id ne $patron->id ) {
1799 $evt = $U->check_perms($requestor->id, $requestor->ws_ou, 'RENEW_CIRC');
1800 if($evt) { $__isrenewal = 0; return $evt; }
1804 # fetch and build the circulation environment
1805 ( $ctx, $evt ) = create_circ_ctx( %$params,
1807 requestor => $requestor,
1810 #fetch_patron_circ_summary => 1,
1811 fetch_copy_statuses => 1,
1812 fetch_copy_locations => 1,
1814 if($evt) { $__isrenewal = 0; return $evt; }
1815 $params->{_ctx} = $ctx;
1817 # make sure they have some renewals left and make sure the circulation exists
1818 ($circ, $evt) = _check_renewal_remaining($ctx);
1819 if($evt) { $__isrenewal = 0; return $evt; }
1820 $ctx->{old_circ} = $circ;
1821 my $renewals = $circ->renewal_remaining - 1;
1823 # run the renew permit script
1824 $evt = _run_renew_scripts($ctx);
1825 if($evt) { $__isrenewal = 0; return $evt; }
1828 #$ctx->{patron} = $ctx->{patron}->id;
1829 $evt = $self->generic_receive($client, $authtoken, $ctx );
1830 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1832 if( !$U->event_equals($evt, 'SUCCESS') ) {
1833 $__isrenewal = 0; return $evt;
1836 # re-fetch the context since objects have changed in the checkin
1837 ( $ctx, $evt ) = create_circ_ctx( %$params,
1839 requestor => $requestor,
1842 #fetch_patron_circ_summary => 1,
1843 fetch_copy_statuses => 1,
1844 fetch_copy_locations => 1,
1846 if($evt) { $__isrenewal = 0; return $evt; }
1847 $params->{_ctx} = $ctx;
1848 $ctx->{renewal_remaining} = $renewals;
1850 # run the circ permit scripts
1851 if( $ctx->{permit_override} ) {
1852 $evt = $U->check_perms(
1853 $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1854 if($evt) { $__isrenewal = 0; return $evt; }
1857 $evt = $self->permit_circ( $client, $authtoken, $params );
1858 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1859 #$ctx->{precat} = 1;
1860 $params->{precat} = 1;
1863 if(!$U->event_equals($evt, 'SUCCESS')) {
1864 if($evt) { $__isrenewal = 0; return $evt; }
1867 $params->{permit_key} = $evt->{payload};
1871 # checkout the item again
1872 $params->{patron} = $ctx->{patron}->id;
1873 $evt = $self->checkout($client, $authtoken, $params );
1875 $logger->activity("user ".$requestor->id." renewl of item ".
1876 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1882 sub _check_renewal_remaining {
1885 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1886 return (undef, $evt) if $evt;
1887 $evt = OpenILS::Event->new(
1888 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1889 return ($circ, $evt);
1892 sub _run_renew_scripts {
1894 my $runner = $ctx->{runner};
1897 $runner->load($scripts{circ_permit_renew});
1898 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1900 my $events = $runner->retrieve('result.events');
1901 $events = [ split(/,/, $events) ];
1902 $logger->activity("circ_permit_renew for user ".
1903 $ctx->{patron}->id." returned events: @$events") if @$events;
1906 push( @allevents, OpenILS::Event->new($_)) for @$events;
1907 return \@allevents if @allevents;