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' );
397 # fetch and build the circulation environment
398 if( !( $ctx = $params->{_ctx}) ) {
400 ( $ctx, $evt ) = create_circ_ctx( %$params,
402 requestor => $requestor,
404 #fetch_patron_circ_summary => 1,
405 fetch_copy_statuses => 1,
406 fetch_copy_locations => 1,
411 my $copy = $ctx->{copy};
413 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
414 return OpenILS::Event->new('COPY_IN_TRANSIT')
415 if $stat == $U->copy_status_from_name('in transit')->id;
418 $ctx->{authtoken} = $authtoken;
421 if( $ctx->{copy} and ($evt = _handle_claims_returned($ctx)) ) {
422 return $evt unless $U->event_equals($evt, 'SUCCESS');
430 # no claims returned circ was found, check if there is any open circ
431 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
432 ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
433 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
438 $ctx->{permit_key} = _cache_permit_key();
439 my $events = _run_permit_scripts($ctx);
442 $evt = override_events($requestor, $requestor->ws_ou,
443 $events, $authtoken, $ctx->{copy}->id, $client);
445 return OpenILS::Event->new('SUCCESS', payload => $ctx->{permit_key} );
451 sub override_events {
453 my( $requestor, $org, $events, $authtoken, $copyid, $conn ) = @_;
454 $events = [ $events ] unless ref($events) eq 'ARRAY';
457 for my $e (@$events) {
458 my $tc = $e->{textcode};
459 next if $tc eq 'SUCCESS';
460 my $ov = "$tc.override";
461 $logger->info("attempting to override event $ov");
462 my $evt = $U->check_perms( $requestor->id, $org, $ov );
470 __PACKAGE__->register_method(
471 method => "check_title_hold",
472 api_name => "open-ils.circ.title_hold.is_possible",
474 Determines if a hold were to be placed by a given user,
475 whether or not said hold would have any potential copies
477 @param authtoken The login session key
478 @param params A hash of named params including:
479 patronid - the id of the hold recipient
480 titleid (brn) - the id of the title to be held
481 depth - the hold range depth (defaults to 0)
484 # XXX add pickup lib to the call to test for perms
486 sub check_title_hold {
487 my( $self, $client, $authtoken, $params ) = @_;
488 my %params = %$params;
489 my $titleid = $params{titleid};
491 my ( $requestor, $patron, $evt ) = $U->checkses_requestor(
492 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
495 my $rangelib = $patron->home_ou;
496 my $depth = $params{depth} || 0;
497 my $pickup = $params{pickup_lib};
499 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
501 my $org = $U->simplereq(
503 'open-ils.actor.org_unit.retrieve',
504 $authtoken, $requestor->home_ou );
510 while( $title = $U->storagereq(
511 'open-ils.storage.biblio.record_entry.ranged_tree',
512 $titleid, $rangelib, $depth, $limit, $offset ) ) {
514 last unless ref($title);
516 for my $cn (@{$title->call_numbers}) {
518 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
520 for my $copy (@{$cn->copies}) {
522 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
524 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
526 requestor => $requestor,
529 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
530 pickup_lib => $pickup,
531 request_lib => $org } );
533 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
544 # Runs the patron and copy permit scripts
545 # if this is a non-cat circulation, the copy permit script
547 sub _run_permit_scripts {
550 my $runner = $ctx->{runner};
551 my $patronid = $ctx->{patron}->id;
552 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
553 my $key = $ctx->{permit_key};
555 my $penalties = $U->update_patron_penalties(
556 authtoken => $ctx->{authtoken},
557 patron => $ctx->{patron}
560 $penalties = $penalties->{fatal_penalties};
562 $logger->info("circ patron penalties user $patronid: @$penalties");
564 if( $ctx->{noncat} ) {
565 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
566 return OpenILS::Event->new('SUCCESS', payload => $key);
570 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
571 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
575 $logger->debug("Exiting circ permit early because request is for hold patron permit");
576 return OpenILS::Event->new('SUCCESS');
579 $runner->load($scripts{circ_permit_copy});
580 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
582 # ---------------------------------------------------------------------
583 # Capture all of the copy permit events
584 # ---------------------------------------------------------------------
585 my $copy_events = $runner->retrieve('result.events');
586 $copy_events = [ split(/,/, $copy_events) ];
587 $ctx->{circ_permit_copy_events} = $copy_events;
588 $logger->activity("circ_permit_copy for copy ".
589 "$barcode returned events: @$copy_events") if @$copy_events;
592 push( @allevents, OpenILS::Event->new($_)) for @$penalties;
593 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
595 my $ae = _check_copy_alert($ctx->{copy});
596 push( @allevents, $ae ) if $ae;
598 return OpenILS::Event->new('SUCCESS', payload => $key) unless (@allevents);
600 # uniquify the events
601 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
602 @allevents = values %hash;
605 $_->{payload} = $ctx->{copy}->status->id
606 if ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
612 sub _check_copy_alert {
614 return OpenILS::Event->new('COPY_ALERT_MESSAGE',
615 payload => $copy->alert_message) if $copy->alert_message;
619 # takes copyid, patronid, and requestor id
620 sub _cache_permit_key {
621 my $key = md5_hex( time() . rand() . "$$" );
622 $logger->debug("Setting circ permit key to $key");
623 $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
627 sub _check_permit_key {
629 $logger->debug("Fetching circ permit key $key");
630 my $k = "oils_permit_key_$key";
631 my $one = $cache_handle->get_cache($k);
632 $cache_handle->delete_cache($k);
633 return ($one) ? 1 : 0;
637 # ------------------------------------------------------------------------------
639 __PACKAGE__->register_method(
640 method => "checkout",
641 api_name => "open-ils.circ.checkout",
644 @param authtoken The login session key
645 @param params A named hash of params including:
647 barcode If no copy is provided, the copy is retrieved via barcode
648 copyid If no copy or barcode is provide, the copy id will be use
649 patron The patron's id
650 noncat True if this is a circulation for a non-cataloted item
651 noncat_type The non-cataloged type id
652 noncat_circ_lib The location for the noncat circ.
653 precat The item has yet to be cataloged
654 dummy_title The temporary title of the pre-cataloded item
655 dummy_author The temporary authr of the pre-cataloded item
656 Default is the home org of the staff member
657 @return The SUCCESS event on success, any other event depending on the error
661 my( $self, $client, $authtoken, $params ) = @_;
664 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
665 my $key = $params->{permit_key};
667 # if this is a renewal, then the requestor does not have to
668 # have checkout privelages
669 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
670 ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
673 if( $params->{patron} ) {
674 ( $patron, $evt ) = $U->fetch_user($params->{patron});
677 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
681 # set the circ lib to the home org of the requestor if not specified
682 my $circlib = (defined($params->{circ_lib})) ?
683 $params->{circ_lib} : $requestor->ws_ou;
686 # Make sure the caller has a valid permit key or is
687 # overriding the permit can
688 if( $params->{permit_override} ) {
689 $evt = $U->check_perms(
690 $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
694 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
695 unless _check_permit_key($key);
698 # if this is a non-cataloged item, check it out and return
699 return _checkout_noncat(
700 $key, $requestor, $patron, %$params ) if $params->{noncat};
702 # if this item has yet to be cataloged, make sure a dummy copy exists
703 ( $params->{copy}, $evt ) = _make_precat_copy(
704 $requestor, $circlib, $params ) if $params->{precat};
708 # fetch and build the circulation environment
709 if( !( $ctx = $params->{_ctx}) ) {
710 ( $ctx, $evt ) = create_circ_ctx( %$params,
712 requestor => $requestor,
713 session => $U->start_db_session(),
715 #fetch_patron_circ_summary => 1,
716 fetch_copy_statuses => 1,
717 fetch_copy_locations => 1,
721 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
723 # if the call doesn't know it's not cataloged..
724 if(!$params->{precat}) {
725 if( $ctx->{copy}->call_number eq '-1' ) {
726 return OpenILS::Event->new('ITEM_NOT_CATALOGED');
731 $copy = $ctx->{copy};
733 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
734 return OpenILS::Event->new('COPY_IN_TRANSIT')
735 if $stat == $U->copy_status_from_name('in transit')->id;
738 # this happens in permit.. but we need to check here for 'offline' requests
739 ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
740 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
742 my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
745 $ctx->{circ_lib} = $circlib;
747 $evt = _run_checkout_scripts($ctx);
751 _build_checkout_circ_object($ctx);
753 $evt = _apply_modified_due_date($ctx);
756 $evt = _commit_checkout_circ_object($ctx);
759 $evt = _update_checkout_copy($ctx);
763 ($holds, $evt) = _handle_related_holds($ctx);
767 $logger->debug("Checkout committing objects with session thread trace: ".$ctx->{session}->session_id);
768 $U->commit_db_session($ctx->{session});
769 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
771 $logger->activity("user ".$requestor->id." successfully checked out item ".
772 $ctx->{copy}->barcode." to user ".$ctx->{patron}->id );
775 # ------------------------------------------------------------------------------
776 # Update the patron penalty info in the DB
777 # ------------------------------------------------------------------------------
778 $U->update_patron_penalties(
779 authtoken => $authtoken,
780 patron => $ctx->{patron} ,
784 return OpenILS::Event->new('SUCCESS',
786 copy => $U->unflesh_copy($ctx->{copy}),
787 circ => $ctx->{circ},
789 holds_fulfilled => $holds,
795 sub _make_precat_copy {
796 my ( $requestor, $circlib, $params ) = @_;
798 my( $copy, undef ) = _find_copy_by_attr(%$params);
801 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
803 $copy->editor($requestor->id);
804 $copy->edit_date('now');
805 $copy->dummy_title($params->{dummy_title});
806 $copy->dummy_author($params->{dummy_author});
808 my $stat = $U->storagereq(
809 'open-ils.storage.direct.asset.copy.update', $copy );
811 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $stat;
815 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
817 my $evt = OpenILS::Event->new(
818 'BAD_PARAMS', desc => "Dummy title or author not provided" )
819 unless ( $params->{dummy_title} and $params->{dummy_author} );
820 return (undef, $evt) if $evt;
822 $copy = Fieldmapper::asset::copy->new;
823 $copy->circ_lib($circlib);
824 $copy->creator($requestor->id);
825 $copy->editor($requestor->id);
826 $copy->barcode($params->{barcode});
827 $copy->call_number(-1); #special CN for precat materials
828 $copy->loan_duration(&PRECAT_LOAN_DURATION);
829 $copy->fine_level(&PRECAT_FINE_LEVEL);
831 $copy->dummy_title($params->{dummy_title});
832 $copy->dummy_author($params->{dummy_author});
834 my $id = $U->storagereq(
835 'open-ils.storage.direct.asset.copy.create', $copy );
836 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
838 $logger->debug("Pre-cataloged copy successfully created");
839 return ($U->fetch_copy($id));
843 sub _run_checkout_scripts {
849 my $runner = $ctx->{runner};
851 $runner->insert('result.durationLevel');
852 $runner->insert('result.durationRule');
853 $runner->insert('result.recurringFinesRule');
854 $runner->insert('result.recurringFinesLevel');
855 $runner->insert('result.maxFine');
857 $runner->load($scripts{circ_duration});
858 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
859 my $duration = $runner->retrieve('result.durationRule');
860 $logger->debug("Circ duration script yielded a duration rule of: $duration");
862 $runner->load($scripts{circ_recurring_fines});
863 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
864 my $recurring = $runner->retrieve('result.recurringFinesRule');
865 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
867 $runner->load($scripts{circ_max_fines});
868 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
869 my $max_fine = $runner->retrieve('result.maxFine');
870 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
872 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
874 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
876 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
879 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
880 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
881 $ctx->{duration_rule} = $duration;
882 $ctx->{recurring_fines_rule} = $recurring;
883 $ctx->{max_fine_rule} = $max_fine;
888 sub _build_checkout_circ_object {
892 my $circ = new Fieldmapper::action::circulation;
893 my $duration = $ctx->{duration_rule};
894 my $max = $ctx->{max_fine_rule};
895 my $recurring = $ctx->{recurring_fines_rule};
896 my $copy = $ctx->{copy};
897 my $patron = $ctx->{patron};
898 my $dur_level = $ctx->{duration_level};
899 my $rec_level = $ctx->{recurring_fines_level};
901 $circ->duration( $duration->shrt ) if ($dur_level == 1);
902 $circ->duration( $duration->normal ) if ($dur_level == 2);
903 $circ->duration( $duration->extended ) if ($dur_level == 3);
905 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
906 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
907 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
909 $circ->duration_rule( $duration->name );
910 $circ->recuring_fine_rule( $recurring->name );
911 $circ->max_fine_rule( $max->name );
912 $circ->max_fine( $max->amount );
914 $circ->fine_interval($recurring->recurance_interval);
915 $circ->renewal_remaining( $duration->max_renewals );
916 $circ->target_copy( $copy->id );
917 $circ->usr( $patron->id );
918 $circ->circ_lib( $ctx->{circ_lib} );
921 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
922 $circ->opac_renewal(1);
923 $circ->renewal_remaining($ctx->{renewal_remaining});
924 $circ->circ_staff($ctx->{requestor}->id);
928 # if the user provided an overiding checkout time,
929 # (e.g. the checkout really happened several hours ago), then
930 # we apply that here. Does this need a perm??
931 if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
932 $logger->debug("circ setting checkout_time to $ds");
933 $circ->xact_start($ds);
936 # if a patron is renewing, 'requestor' will be the patron
937 $circ->circ_staff($ctx->{requestor}->id );
938 _set_circ_due_date($circ);
939 $ctx->{circ} = $circ;
942 sub _apply_modified_due_date {
944 my $circ = $ctx->{circ};
948 if( $ctx->{due_date} ) {
950 my $evt = $U->check_perms(
951 $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
954 my $ds = _create_date_stamp($ctx->{due_date});
955 $logger->debug("circ modifying due_date to $ds");
956 $circ->due_date($ds);
960 # if the due_date lands on a day when the location is closed
961 my $copy = $ctx->{copy};
964 $logger->info("circ searching for closed date overlap on lib ".
965 $copy->circ_lib->id ." with an item due date of ".$circ->due_date );
967 my $dateinfo = $ctx->{session}->request(
968 'open-ils.storage.actor.org_unit.closed_date.overlap',
969 $copy->circ_lib->id, $circ->due_date )->gather(1);
973 $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
974 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
976 # XXX make the behavior more dynamic
977 # for now, we just push the due date to after the close date
978 $circ->due_date($dateinfo->{end});
985 sub _create_date_stamp {
986 my $datestring = shift;
987 return undef unless $datestring;
988 $datestring = clense_ISO8601($datestring);
989 $logger->debug("circ created date stamp => $datestring");
993 sub _create_due_date {
994 my $duration = shift;
996 my ($sec,$min,$hour,$mday,$mon,$year) =
997 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
998 $year += 1900; $mon += 1;
999 my $due_date = sprintf(
1000 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1001 $year, $mon, $mday, $hour, $min, $sec);
1005 sub _set_circ_due_date {
1008 my $dd = _create_due_date($circ->duration);
1009 $logger->debug("Checkout setting due date on circ to: $dd");
1010 $circ->due_date($dd);
1013 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
1014 sub _update_checkout_copy {
1017 my $copy = $ctx->{copy};
1019 my $s = $U->copy_status_from_name('checked out');
1020 $copy->status( $s->id ) if $s;
1022 my $evt = $U->update_copy( session => $ctx->{session},
1023 copy => $copy, editor => $ctx->{requestor}->id );
1024 return (undef,$evt) if $evt;
1029 # commits the circ object to the db then fleshes the circ with rules objects
1030 sub _commit_checkout_circ_object {
1033 my $circ = $ctx->{circ};
1037 my $r = $ctx->{session}->request(
1038 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
1040 return $U->DB_UPDATE_FAILED($circ) unless $r;
1042 $logger->debug("Created a new circ object in checkout: $r");
1045 $circ->duration_rule($ctx->{duration_rule});
1046 $circ->max_fine_rule($ctx->{max_fine_rule});
1047 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
1053 # sees if there are any holds that this copy
1054 sub _handle_related_holds {
1057 my $copy = $ctx->{copy};
1058 my $patron = $ctx->{patron};
1059 my $holds = $holdcode->fetch_related_holds($copy->id);
1063 # XXX We should only fulfill one hold here...
1064 # XXX If a hold was transited to the user who is checking out
1065 # the item, we need to make sure that hold is what's grabbed
1066 if(ref($holds) && @$holds) {
1068 # for now, just sort by id to get what should be the oldest hold
1069 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1070 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
1073 my $hold = $holds->[0];
1075 $logger->debug("Related hold found in checkout: " . $hold->id );
1077 $hold->current_copy($copy->id); # just make sure it's set
1078 # if the hold was never officially captured, capture it.
1079 $hold->capture_time('now') unless $hold->capture_time;
1080 $hold->fulfillment_time('now');
1081 my $r = $ctx->{session}->request(
1082 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
1083 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
1084 push( @fulfilled, $hold->id );
1088 return (\@fulfilled, undef);
1091 sub _checkout_noncat {
1092 my ( $key, $requestor, $patron, %params ) = @_;
1093 my( $circ, $circlib, $evt );
1096 $circlib = $params{noncat_circ_lib} || $requestor->ws_ou;
1098 my $count = $params{noncat_count} || 1;
1099 my $cotime = _create_date_stamp($params{checkout_time}) || "";
1100 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1102 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1103 $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
1104 return $evt if $evt;
1107 return OpenILS::Event->new(
1108 'SUCCESS', payload => { noncat_circ => $circ } );
1112 __PACKAGE__->register_method(
1113 method => "generic_receive",
1114 api_name => "open-ils.circ.checkin",
1117 Generic super-method for handling all copies
1118 @param authtoken The login session key
1119 @param params Hash of named parameters including:
1120 barcode - The copy barcode
1121 force - If true, copies in bad statuses will be checked in and give good statuses
1126 __PACKAGE__->register_method(
1127 method => "generic_receive",
1128 api_name => "open-ils.circ.checkin.override",
1129 signature => q/@see open-ils.circ.checkin/
1132 sub generic_receive {
1133 my( $self, $conn, $authtoken, $params ) = @_;
1134 my( $ctx, $requestor, $evt );
1136 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1137 ( $requestor, $evt ) = $U->checksesperm(
1138 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1139 return $evt if $evt;
1141 # load up the circ objects
1142 if( !( $ctx = $params->{_ctx}) ) {
1143 ( $ctx, $evt ) = create_circ_ctx( %$params,
1144 requestor => $requestor,
1145 session => $U->start_db_session(),
1147 fetch_copy_statuses => 1,
1148 fetch_copy_locations => 1,
1151 return $evt if $evt;
1153 $ctx->{override} = 1 if $self->api_name =~ /override/o;
1154 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1155 $ctx->{authtoken} = $authtoken;
1156 my $session = $ctx->{session};
1158 my $copy = $ctx->{copy};
1159 $U->unflesh_copy($copy);
1160 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $copy;
1162 $logger->info("Checkin copy called by user ".
1163 $requestor->id." for copy ".$copy->id);
1165 # ------------------------------------------------------------------------------
1166 # Update the patron penalty info in the DB
1167 # ------------------------------------------------------------------------------
1168 $U->update_patron_penalties(
1169 authtoken => $authtoken,
1170 patron => $ctx->{patron},
1174 return $self->checkin_do_receive($conn, $ctx);
1177 sub checkin_do_receive {
1179 my( $self, $connection, $ctx ) = @_;
1182 my $copy = $ctx->{copy};
1183 my $session = $ctx->{session};
1184 my $requestor = $ctx->{requestor};
1185 my $change = 0; # did we actually do anything?
1190 # does the copy have an attached alert message?
1191 my $ae = _check_copy_alert($copy);
1192 push(@eventlist, $ae) if $ae;
1194 # is the copy is an a status we can't automatically resolve?
1195 $evt = _checkin_check_copy_status($ctx);
1196 push( @eventlist, $evt ) if $evt;
1199 # - see if the copy has an open circ attached
1200 ($ctx->{circ}, $evt) = $U->fetch_open_circulation($copy->id);
1201 return $evt if ($evt and $__isrenewal); # renewals require a circulation
1203 $circ = $ctx->{circ};
1205 # if the circ is marked as 'claims returned', add the event to the list
1206 push( @eventlist, 'CIRC_CLAIMS_RETURNED' )
1207 if ($circ and $circ->stop_fines and $circ->stop_fines eq 'CLAIMSRETURNED');
1211 if($ctx->{override}) {
1212 $evt = override_events($requestor, $requestor->ws_ou, \@eventlist );
1213 return $evt if $evt;
1219 ($ctx->{transit}) = $U->fetch_open_transit_by_copy($copy->id);
1221 if( $ctx->{circ} ) {
1223 # There is an open circ on this item, close it out.
1225 $evt = _checkin_handle_circ($ctx);
1226 return $evt if $evt;
1228 } elsif( $ctx->{transit} ) {
1230 # is this item currently in transit?
1232 $evt = $transcode->transit_receive( $copy, $requestor, $session );
1233 my $holdtrans = $evt->{holdtransit};
1234 ($ctx->{hold}) = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1236 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1238 # either an error occurred or a ROUTE_ITEM was generated and the
1239 # item must be forwarded on to its destination.
1240 return _checkin_flesh_event($ctx, $evt);
1244 # Transit has been closed, now let's see if the copy's original
1245 # status is something the staff should be warned of
1246 my $e = _checkin_check_copy_status($ctx);
1251 # copy was received as a hold transit. Copy is at target lib
1252 # and hold transit is complete. We're done here...
1253 $U->commit_db_session($session);
1254 return _checkin_flesh_event($ctx, $evt);
1260 # ------------------------------------------------------------------------------
1261 # Circulations and transits are now closed where necessary. Now go on to see if
1262 # this copy can fulfill a hold or needs to be routed to a different location
1263 # ------------------------------------------------------------------------------
1266 # If it's a renewal, we're done
1269 #my ($cc, $ee) = _reshelve_copy($ctx);
1271 #delete $$ctx{force};
1272 $U->commit_db_session($session);
1273 return OpenILS::Event->new('SUCCESS');
1276 # Now, let's see if this copy is needed for a hold
1277 my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor );
1281 $ctx->{hold} = $hold;
1284 # Capture the hold with this copy
1285 return $evt if ($evt = _checkin_capture_hold($ctx));
1287 if( $hold->pickup_lib == $requestor->ws_ou ) {
1289 # This hold was captured in the correct location
1290 $evt = OpenILS::Event->new('SUCCESS');
1294 # Hold needs to be picked up elsewhere. Build a hold
1295 # transit and route the item.
1296 return $evt if ($evt =_checkin_build_hold_transit($ctx));
1297 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1300 } else { # not needed for a hold
1302 if( $copy->circ_lib == $requestor->ws_ou ) {
1304 # Copy is in the right place.
1305 $evt = OpenILS::Event->new('SUCCESS');
1307 # if the item happens to be a pre-cataloged item, send it
1308 # to cataloging and return the event
1309 my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1310 return $err if $err;
1316 # Copy wants to go home. Transit it there.
1317 return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1318 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1324 # ------------------------------------------------------------------
1325 # if the copy is not in a state that should persist,
1326 # set the copy to reshelving if it's not already there
1327 # ------------------------------------------------------------------
1328 my ($c, $e) = _reshelve_copy($ctx);
1330 $change = $c unless $change;
1334 $evt = OpenILS::Event->new('NO_CHANGE');
1335 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id)
1336 if( $copy->status == $U->copy_status_from_name('on holds shelf')->id );
1340 $U->commit_db_session($session);
1343 $logger->activity("checkin by user ".$requestor->id." on item ".
1344 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1346 return _checkin_flesh_event($ctx, $evt);
1349 sub _reshelve_copy {
1352 my $copy = $ctx->{copy};
1353 my $reqr = $ctx->{requestor};
1354 my $session = $ctx->{session};
1355 my $force = $ctx->{force};
1357 my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1360 $stat != $U->copy_status_from_name('on holds shelf')->id and
1361 $stat != $U->copy_status_from_name('available')->id and
1362 $stat != $U->copy_status_from_name('cataloging')->id and
1363 $stat != $U->copy_status_from_name('in transit')->id and
1364 $stat != $U->copy_status_from_name('reshelving')->id) ) {
1366 $copy->status( $U->copy_status_from_name('reshelving')->id );
1368 my $evt = $U->update_copy(
1370 editor => $reqr->id,
1371 session => $session,
1382 # returns undef if there are no 'open' claims-returned circs attached
1383 # to the given copy. if there is an open claims-returned circ,
1384 # then we check for override mode. if in override, mark the claims-returned
1385 # circ as checked in. if not, return event.
1386 sub _handle_claims_returned {
1388 my $copy = $ctx->{copy};
1390 my $CR = _fetch_open_claims_returned($copy->id);
1391 return undef unless $CR;
1393 # - If the caller has set the override flag, we will check the item in
1394 if($ctx->{override}) {
1396 $CR->checkin_time('now');
1397 $CR->checkin_lib($ctx->{requestor}->ws_ou);
1398 $CR->checkin_staff($ctx->{requestor}->id);
1400 my $stat = $U->storagereq(
1401 'open-ils.storage.direct.action.circulation.update', $CR);
1402 return $U->DB_UPDATE_FAILED($CR) unless $stat;
1403 return OpenILS::Event->new('SUCCESS');
1406 # - if not in override mode, return the CR event
1407 return OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1412 sub _fetch_open_claims_returned {
1414 my $trans = $U->storagereq(
1415 'open-ils.storage.direct.action.circulation.search_where',
1417 target_copy => $copyid,
1418 stop_fines => 'CLAIMSRETURNED',
1419 checkin_time => undef,
1422 return $$trans[0] if $trans && $$trans[0];
1426 # - if the copy is has the 'in process' status, set it to reshelving
1427 #sub _check_in_process {
1430 #my $copy = $ctx->{copy};
1431 #my $reqr = $ctx->{requestor};
1432 #my $ses = $ctx->{session};
1434 #my $stat = $U->copy_status_from_name('in process');
1435 #my $rstat = $U->copy_status_from_name('reshelving');
1437 #if( $stat->id == $copy->status->id ) {
1438 #$logger->info("marking 'in-process' copy ".$copy->id." as 'reshelving'");
1439 #$copy->status( $rstat->id );
1440 #my $evt = $U->update_copy(
1442 #editor => $reqr->id,
1445 #return $evt if $evt;
1447 #$copy->status( $rstat ); # - reflesh the copy status
1453 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1454 sub _checkin_handle_precat {
1457 my $copy = $ctx->{copy};
1462 my $catstat = $U->copy_status_from_name('cataloging');
1464 if( $ctx->{precat} ) {
1466 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1468 if( $copy->status != $catstat->id ) {
1469 $copy->status($catstat->id);
1471 return (undef, 0, $errevt) if (
1472 $errevt = $U->update_copy(
1474 editor => $ctx->{requestor}->id,
1475 session => $ctx->{session} ));
1481 return ($evt, $change, undef);
1485 # returns the appropriate event for the given copy status
1486 # if the copy is not in a 'special' status, undef is returned
1487 sub _checkin_check_copy_status {
1489 my $copy = $ctx->{copy};
1490 my $reqr = $ctx->{requestor};
1491 my $ses = $ctx->{session};
1497 my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1500 if( $status == $U->copy_status_from_name('available')->id ||
1501 $status == $U->copy_status_from_name('checked out')->id ||
1502 $status == $U->copy_status_from_name('in process')->id ||
1503 $status == $U->copy_status_from_name('in transit')->id ||
1504 $status == $U->copy_status_from_name('reshelving')->id );
1506 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1507 if( $status == $U->copy_status_from_name('lost')->id );
1509 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1510 if( $status == $U->copy_status_from_name('missing')->id );
1512 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1516 # my $rstat = $U->copy_status_from_name('reshelving');
1517 # my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1519 # if( $stat == $U->copy_status_from_name('lost')->id ) {
1521 # $evt = OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy );
1523 # } elsif( $stat == $U->copy_status_from_name('missing')->id) {
1525 # $evt = OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy );
1528 # return (undef,$evt) if(!$ctx->{override});
1530 # # we're are now going to attempt to override the failure
1531 # # and set the copy to reshelving
1533 # my $copyid = $copy->id;
1534 # my $userid = $reqr->id;
1537 # # - make sure we have permission
1538 # $e = $U->check_perms( $reqr->id,
1539 # $copy->circ_lib, 'COPY_STATUS_LOST.override');
1540 # return (undef,$e) if $e;
1541 # $copy->status( $rstat->id );
1543 # # XXX if no fines are owed in the circ, close it out - will this happen later anyway?
1544 # #my $circ = $U->storagereq(
1545 # # 'open-ils.storage.direct.action.circulation
1547 # $logger->activity("user $userid overriding 'lost' copy status for copy $copyid");
1549 # } elsif( $ismissing ) {
1551 # # - make sure we have permission
1552 # $e = $U->check_perms( $reqr->id,
1553 # $copy->circ_lib, 'COPY_STATUS_MISSING.override');
1554 # return (undef,$e) if $e;
1555 # $copy->status( $rstat->id );
1556 # $logger->activity("user $userid overriding 'missing' copy status for copy $copyid");
1559 # if( $islost or $ismissing ) {
1561 # # - update the copy with the new status
1562 # $evt = $U->update_copy(
1564 # editor => $reqr->id,
1567 # return (undef,$evt) if $evt;
1568 # $copy->status( $rstat );
1576 # Just gets the copy back home. Returns undef on success, event on error
1577 sub _checkin_build_generic_copy_transit {
1580 my $requestor = $ctx->{requestor};
1581 my $copy = $ctx->{copy};
1582 my $transit = Fieldmapper::action::transit_copy->new;
1583 my $session = $ctx->{session};
1585 $logger->activity("User ". $requestor->id ." creating a ".
1586 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1588 $transit->source($requestor->ws_ou);
1589 $transit->dest($copy->circ_lib);
1590 $transit->target_copy($copy->id);
1591 $transit->source_send_time('now');
1592 $transit->copy_status($copy->status);
1594 $logger->debug("Creating new copy_transit in DB");
1596 my $s = $session->request(
1597 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1598 return $U->DB_UPDATE_FAILED($transit) unless $s;
1600 $logger->info("Checkin copy successfully created new transit: $s");
1602 $copy->status($U->copy_status_from_name('in transit')->id );
1604 return $U->update_copy( copy => $copy,
1605 editor => $requestor->id, session => $session );
1610 # returns event on error, undef on success
1611 sub _checkin_build_hold_transit {
1614 my $copy = $ctx->{copy};
1615 my $hold = $ctx->{hold};
1616 my $trans = Fieldmapper::action::hold_transit_copy->new;
1618 $trans->hold($hold->id);
1619 $trans->source($ctx->{requestor}->ws_ou);
1620 $trans->dest($hold->pickup_lib);
1621 $trans->source_send_time("now");
1622 $trans->target_copy($copy->id);
1623 $trans->copy_status($copy->status);
1625 my $id = $ctx->{session}->request(
1626 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1627 return $U->DB_UPDATE_FAILED($trans) unless $id;
1629 $logger->info("Checkin copy successfully created hold transit: $id");
1631 $copy->status($U->copy_status_from_name('in transit')->id );
1632 return $U->update_copy( copy => $copy,
1633 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1636 # Returns event on error, undef on success
1637 sub _checkin_capture_hold {
1639 my $copy = $ctx->{copy};
1640 my $hold = $ctx->{hold};
1642 $logger->debug("Checkin copy capturing hold ".$hold->id);
1644 $hold->current_copy($copy->id);
1645 $hold->capture_time('now');
1647 my $stat = $ctx->{session}->request(
1648 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1649 return $U->DB_UPDATE_FAILED($hold) unless $stat;
1651 $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1653 return $U->update_copy( copy => $copy,
1654 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1657 # fleshes an event with the relevant objects from the context
1658 sub _checkin_flesh_event {
1663 $payload->{copy} = $U->unflesh_copy($ctx->{copy});
1664 $payload->{record} = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1665 $payload->{circ} = $ctx->{circ} if $ctx->{circ};
1666 $payload->{transit} = $ctx->{transit} if $ctx->{transit};
1667 $payload->{hold} = $ctx->{hold} if $ctx->{hold};
1669 $evt->{payload} = $payload;
1674 # Closes out the circulation, puts the copy into reshelving.
1675 # Voids any bills attached to this circ after the backdate time
1676 # if a backdate is provided
1677 sub _checkin_handle_circ {
1681 my $circ = $ctx->{circ};
1682 my $copy = $ctx->{copy};
1683 my $requestor = $ctx->{requestor};
1684 my $session = $ctx->{session};
1688 $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1690 # backdate the circ if necessary
1691 if(my $backdate = $ctx->{backdate}) {
1692 return $evt if ($evt =
1693 _checkin_handle_backdate($backdate, $circ, $requestor, $session, 1));
1697 if(!$circ->stop_fines) {
1698 $circ->stop_fines('CHECKIN');
1699 $circ->stop_fines('RENEW') if $__isrenewal;
1700 $circ->stop_fines_time('now');
1703 # see if there are any fines owed on this circ. if not, close it
1704 ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1705 return $evt if $evt;
1706 $circ->xact_finish('now') if( $obt->balance_owed != 0 );
1708 # Set the checkin vars since we have the item
1709 $circ->checkin_time('now');
1710 $circ->checkin_staff($requestor->id);
1711 $circ->checkin_lib($requestor->ws_ou);
1713 $evt = _set_copy_reshelving($copy, $requestor->id, $ctx->{session});
1714 return $evt if $evt;
1716 # $copy->status($U->copy_status_from_name('reshelving')->id);
1717 # $evt = $U->update_copy( session => $session,
1718 # copy => $copy, editor => $requestor->id );
1719 # return $evt if $evt;
1721 $ctx->{session}->request(
1722 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1727 sub _set_copy_reshelving {
1728 my( $copy, $reqr, $session ) = @_;
1730 $logger->info("Setting copy ".$copy->id." to reshelving");
1731 $copy->status($U->copy_status_from_name('reshelving')->id);
1733 my $evt = $U->update_copy(
1734 session => $session,
1738 return $evt if $evt;
1741 # returns event on error, undef on success
1742 # This voids all bills attached to the given circulation that occurred
1743 # after the backdate
1744 # THIS DOES NOT CLOSE THE CIRC if there are no more fines on the item
1745 sub _checkin_handle_backdate {
1746 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1748 $logger->activity("User ".$requestor->id.
1749 " backdating circ [".$circ->target_copy."] to date: $backdate");
1751 my $bills = $session->request( # XXX Verify this call is correct
1752 "open-ils.storage.direct.money.billing.search_where.atomic",
1753 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1756 for my $bill (@$bills) {
1758 my $s = $session->request(
1759 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1760 return $U->DB_UPDATE_FAILED($bill) unless $s;
1764 # if the caller elects to attempt to close the circulation
1765 # transaction, then it will be closed if there are not further
1766 # charges on the transaction
1768 #my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1769 #return $evt if $evt;
1770 #$circ->xact_finish($backdate) if $obt->balance_owed <= 0;
1777 sub _find_patron_from_params {
1785 if(my $barcode = $params->{barcode}) {
1786 $logger->debug("circ finding user from params with barcode $barcode");
1787 ($copy, $evt) = $U->fetch_copy_by_barcode($barcode);
1788 return (undef, undef, $evt) if $evt;
1789 ($circ, $evt) = $U->fetch_open_circulation($copy->id);
1790 return (undef, undef, $evt) if $evt;
1791 ($patron, $evt) = $U->fetch_user($circ->usr);
1792 return (undef, undef, $evt) if $evt;
1794 return ($patron, $copy);
1798 # ------------------------------------------------------------------------------
1800 __PACKAGE__->register_method(
1802 api_name => "open-ils.circ.renew.override",
1803 signature => q/@see open-ils.circ.renew/,
1807 __PACKAGE__->register_method(
1809 api_name => "open-ils.circ.renew",
1810 notes => <<" NOTES");
1811 PARAMS( authtoken, circ => circ_id );
1812 open-ils.circ.renew(login_session, circ_object);
1813 Renews the provided circulation. login_session is the requestor of the
1814 renewal and if the logged in user is not the same as circ->usr, then
1815 the logged in user must have RENEW_CIRC permissions.
1819 my( $self, $client, $authtoken, $params ) = @_;
1822 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1825 $params->{override} = 1 if $self->api_name =~ /override/o;
1827 # fetch the patron object one way or another
1828 if( $params->{patron} ) {
1829 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1830 if($evt) { $__isrenewal = 0; return $evt; }
1832 } elsif( $params->{patron_barcode} ) {
1833 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1834 if($evt) { $__isrenewal = 0; return $evt; }
1837 ($patron, $copy, $evt) = _find_patron_from_params($params);
1838 return $evt if $evt;
1839 $params->{copy} = $copy;
1842 # verify our login session
1843 ($requestor, $evt) = $U->checkses($authtoken);
1844 if($evt) { $__isrenewal = 0; return $evt; }
1846 # make sure we have permission to perform a renewal
1847 if( $requestor->id ne $patron->id ) {
1848 $evt = $U->check_perms($requestor->id, $requestor->ws_ou, 'RENEW_CIRC');
1849 if($evt) { $__isrenewal = 0; return $evt; }
1853 # fetch and build the circulation environment
1854 ( $ctx, $evt ) = create_circ_ctx( %$params,
1856 requestor => $requestor,
1859 #fetch_patron_circ_summary => 1,
1860 fetch_copy_statuses => 1,
1861 fetch_copy_locations => 1,
1863 if($evt) { $__isrenewal = 0; return $evt; }
1864 $params->{_ctx} = $ctx;
1866 # make sure they have some renewals left and make sure the circulation exists
1867 ($circ, $evt) = _check_renewal_remaining($ctx);
1868 if($evt) { $__isrenewal = 0; return $evt; }
1869 $ctx->{old_circ} = $circ;
1870 my $renewals = $circ->renewal_remaining - 1;
1872 # run the renew permit script
1873 $evt = _run_renew_scripts($ctx);
1874 if($evt) { $__isrenewal = 0; return $evt; }
1877 #$ctx->{patron} = $ctx->{patron}->id;
1878 $evt = $self->generic_receive($client, $authtoken, $ctx );
1879 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1881 if( !$U->event_equals($evt, 'SUCCESS') ) {
1882 $__isrenewal = 0; return $evt;
1885 # re-fetch the context since objects have changed in the checkin
1886 ( $ctx, $evt ) = create_circ_ctx( %$params,
1888 requestor => $requestor,
1891 #fetch_patron_circ_summary => 1,
1892 fetch_copy_statuses => 1,
1893 fetch_copy_locations => 1,
1895 if($evt) { $__isrenewal = 0; return $evt; }
1896 $params->{_ctx} = $ctx;
1897 $ctx->{renewal_remaining} = $renewals;
1899 # run the circ permit scripts
1900 if( $ctx->{permit_override} ) {
1901 $evt = $U->check_perms(
1902 $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1903 if($evt) { $__isrenewal = 0; return $evt; }
1906 $evt = $self->permit_circ( $client, $authtoken, $params );
1907 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1908 #$ctx->{precat} = 1;
1909 $params->{precat} = 1;
1912 if(!$U->event_equals($evt, 'SUCCESS')) {
1913 if($evt) { $__isrenewal = 0; return $evt; }
1916 $params->{permit_key} = $evt->{payload};
1920 # checkout the item again
1921 $params->{patron} = $ctx->{patron}->id;
1922 $evt = $self->checkout($client, $authtoken, $params );
1924 $logger->activity("user ".$requestor->id." renewl of item ".
1925 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1931 sub _check_renewal_remaining {
1934 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1935 return (undef, $evt) if $evt;
1936 $evt = OpenILS::Event->new(
1937 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1938 return ($circ, $evt);
1941 sub _run_renew_scripts {
1943 my $runner = $ctx->{runner};
1946 $runner->load($scripts{circ_permit_renew});
1947 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1949 my $events = $runner->retrieve('result.events');
1950 $events = [ split(/,/, $events) ];
1951 $logger->activity("circ_permit_renew for user ".
1952 $ctx->{patron}->id." returned events: @$events") if @$events;
1955 push( @allevents, OpenILS::Event->new($_)) for @$events;
1956 return \@allevents if @allevents;