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);
15 use OpenILS::Utils::Editor qw/:funcs/;
17 use DateTime::Format::ISO8601;
18 use OpenSRF::Utils qw/:datetime/;
19 use OpenILS::Application::Circ::ScriptBuilder;
21 $Data::Dumper::Indent = 0;
22 my $U = "OpenILS::Application::AppUtils";
23 my $holdcode = "OpenILS::Application::Circ::Holds";
24 my $transcode = "OpenILS::Application::Circ::Transit";
26 my %scripts; # - circulation script filenames
27 my $script_libs; # - any additional script libraries
28 #my %cache; # - db objects cache
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);
74 "Loaded rules scripts for circ: " .
75 "circ permit patron = $p, ".
76 "circ permit copy = $c, ".
77 "circ duration = $d, ".
78 "circ recurring fines = $f, " .
79 "circ max fines = $m, ".
80 "circ renew permit = $pr. ".
85 # ------------------------------------------------------------------------------
86 # Loads the necessary circ objects and pushes them into the script environment
87 # Returns ( $data, $evt ). if $evt is defined, then an
88 # unexpedted event occurred and should be dealt with / returned to the caller
89 # ------------------------------------------------------------------------------
97 $ctx->{copy_id} = $ctx->{copyid};
98 $ctx->{patron_id} = $ctx->{patronid};
99 $ctx->{copy_barcode} = $ctx->{barcode};
100 $ctx->{fetch_patron_circ_info} = 1;
102 my @evts = OpenILS::Application::Circ::ScriptBuilder->build($ctx);
104 if(!$params{noncat}) {
105 if( @evts and grep { $_->{textcode} eq 'ASSET_COPY_NOT_FOUND' } @evts) {
108 $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
112 _build_circ_script_runner($ctx);
117 # $evt = _ctx_add_patron_objects($ctx, %params);
118 # return (undef,$evt) if $evt;
120 # if(!$params{noncat}) {
121 # if( $evt = _ctx_add_copy_objects($ctx, %params) ) {
122 # $ctx->{precat} = 1 if($evt->{textcode} eq 'ASSET_COPY_NOT_FOUND')
124 # $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
128 # _doctor_patron_object($ctx) if $ctx->{patron};
129 # _doctor_copy_object($ctx) if $ctx->{copy};
131 # if(!$ctx->{no_runner}) {
132 # _build_circ_script_runner($ctx);
133 # _add_script_runner_methods($ctx);
140 #sub _ctx_add_patron_objects {
141 # my( $ctx, %params) = @_;
144 # $cache{group_tree} = $U->fetch_permission_group_tree() unless $cache{group_tree};
145 # $ctx->{group_tree} = $cache{group_tree};
147 # $ctx->{patron_circ_summary} =
148 # $U->fetch_patron_circ_summary($ctx->{patron}->id)
149 # if $params{fetch_patron_circsummary};
155 #sub _find_copy_by_attr {
160 # my $copy = $params{copy} || undef;
165 # $U->fetch_copy($params{copyid}) if $params{copyid};
166 # return (undef,$evt) if $evt;
170 # $U->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
171 # return (undef,$evt) if $evt;
174 # return ( $copy, $evt );
177 #sub _ctx_add_copy_objects {
178 # my($ctx, %params) = @_;
183 # $cache{copy_statuses} = $U->fetch_copy_statuses
184 # if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
186 # $cache{copy_locations} = $U->fetch_copy_locations
187 # if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
189 # $ctx->{copy_statuses} = $cache{copy_statuses};
190 # $ctx->{copy_locations} = $cache{copy_locations};
192 # ($copy, $evt) = _find_copy_by_attr(%params);
193 # return $evt if $evt;
195 # if( $copy and !$ctx->{title} ) {
197 # my $r = $RECORD_FROM_COPY_CACHE{$copy->id};
198 # ($r, $evt) = $U->fetch_record_by_copy( $copy->id ) unless $r;
199 # return $evt if $evt;
200 # $RECORD_FROM_COPY_CACHE{$copy->id} = $r;
202 # $ctx->{title} = $r;
203 # $ctx->{copy} = $copy;
205 # ($ctx->{volume}) = $U->fetch_callnumber($copy->call_number);
206 # $ctx->{recordDescriptor} = $U->storagereq(
207 # 'open-ils.storage.direct.metabib.record_descriptor.search_where',
208 # { record => $ctx->{title}->id });
217 ## ------------------------------------------------------------------------------
218 ## Fleshes parts of the patron object
219 ## ------------------------------------------------------------------------------
220 #sub _doctor_copy_object {
223 # my $copy = $ctx->{copy} || return undef;
225 # $logger->debug("Doctoring copy object...");
227 # # set the copy status to a status name
228 # $copy->status( _get_copy_status( $copy, $ctx->{copy_statuses} ) );
230 # # set the copy location to the location object
231 # $copy->location( _get_copy_location( $copy, $ctx->{copy_locations} ) );
233 # $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
238 ## ------------------------------------------------------------------------------
239 ## Fleshes parts of the patron object
240 ## ------------------------------------------------------------------------------
241 #sub _doctor_patron_object {
244 # my $patron = $ctx->{patron} || return undef;
246 # # set the patron ptofile to the profile name
247 # $patron->profile( _get_patron_profile(
248 # $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
250 # # flesh the org unit
252 # $U->fetch_org_unit( $patron->home_ou ) ) if $patron;
256 ## recurse and find the patron profile name from the tree
257 ## another option would be to grab the groups for the patron
258 ## and cycle through those until the "profile" group has been found
259 #sub _get_patron_profile {
260 # my( $patron, $group_tree ) = @_;
261 # return $group_tree if ($group_tree->id eq $patron->profile);
262 # return undef unless ($group_tree->children);
264 # for my $child (@{$group_tree->children}) {
265 # my $ret = _get_patron_profile( $patron, $child );
266 # return $ret if $ret;
271 #sub _get_copy_status {
272 # my( $copy, $cstatus ) = @_;
275 # for my $status (@$cstatus) {
276 # $s = $status if( $status->id eq $copy->status )
278 # $logger->debug("Retrieving copy status: " . $s->name) if $s;
282 #sub _get_copy_location {
283 # my( $copy, $locations ) = @_;
286 # for my $loc (@$locations) {
287 # $l = $loc if $loc->id eq $copy->location;
289 # $logger->debug("Retrieving copy location: " . $l->name ) if $l;
294 # ------------------------------------------------------------------------------
295 # Constructs and shoves data into the script environment
296 # ------------------------------------------------------------------------------
297 sub _build_circ_script_runner {
301 $logger->debug("Loading script environment for circulation");
304 my $runner = $ctx->{runner};
307 $runner->insert('environment.isRenewal', 1);
309 $runner->insert('environment.isRenewal', undef);
312 if($ctx->{ishold} ) {
313 $runner->insert('environment.isHold', 1);
315 $runner->insert('environment.isHold', undef)
318 if( $ctx->{noncat} ) {
319 $runner->insert('environment.isNonCat', 1);
320 $runner->insert('environment.nonCatType', $ctx->{noncat_type});
322 $runner->insert('environment.isNonCat', undef);
326 $logger->debug("Loading circ script lib path $_");
327 $runner->add_path( $_ );
339 # for(@$script_libs) {
340 # $logger->debug("Loading circ script lib path $_");
341 # $runner->add_path( $_ );
344 # # Note: inserting the number 0 into the script turns into the
345 # # string "0", and thus evaluates to true in JS land
346 # # inserting undef will insert "", which evaluates to false
348 # $runner->insert( 'environment.patron', $ctx->{patron}, 1);
349 # $runner->insert( 'environment.record', $ctx->{title}, 1);
350 # $runner->insert( 'environment.copy', $ctx->{copy}, 1);
351 # $runner->insert( 'environment.volume', $ctx->{volume}, 1);
352 # $runner->insert( 'environment.recordDescriptor', $ctx->{recordDescriptor}, 1);
353 # $runner->insert( 'environment.requestor', $ctx->{requestor}, 1);
355 # # circ script result
356 # $runner->insert( 'result', {} );
357 # #$runner->insert( 'result.event', 'SUCCESS' );
358 # $runner->insert( 'result.events', [] );
361 # $runner->insert('environment.isRenewal', 1);
363 # $runner->insert('environment.isRenewal', undef);
366 # if($ctx->{ishold} ) {
367 # $runner->insert('environment.isHold', 1);
369 # $runner->insert('environment.isHold', undef)
372 # if( $ctx->{noncat} ) {
373 # $runner->insert('environment.isNonCat', 1);
374 # $runner->insert('environment.nonCatType', $ctx->{noncat_type});
376 # $runner->insert('environment.isNonCat', undef);
379 # if(ref($ctx->{patron_circ_summary})) {
380 # $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
383 # $ctx->{runner} = $runner;
394 #sub _add_script_runner_methods {
397 # my $runner = $ctx->{runner};
399 # if( $ctx->{copy} ) {
401 # # allows a script to fetch a hold that is currently targeting the
403 # $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
405 # my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
406 # $hold = undef unless $hold;
407 # $runner->insert( $key, $hold, 1 );
413 # ------------------------------------------------------------------------------
416 __PACKAGE__->register_method(
417 method => "permit_circ",
418 api_name => "open-ils.circ.checkout.permit",
420 Determines if the given checkout can occur
421 @param authtoken The login session key
422 @param params A trailing hash of named params including
423 barcode : The copy barcode,
424 patron : The patron the checkout is occurring for,
425 renew : true or false - whether or not this is a renewal
426 @return The event that occurred during the permit check.
429 __PACKAGE__->register_method (
430 method => 'permit_circ',
431 api_name => 'open-ils.circ.checkout.permit.override',
432 signature => q/@see open-ils.circ.checkout.permit/,
436 my( $self, $client, $authtoken, $params ) = @_;
439 my $override = $params->{override} = 1 if $self->api_name =~ /override/o;
441 my ( $requestor, $patron, $ctx, $evt, $circ );
443 # check permisson of the requestor
444 ( $requestor, $patron, $evt ) =
445 $U->checkses_requestor(
446 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
450 # fetch and build the circulation environment
451 if( !( $ctx = $params->{_ctx}) ) {
453 ( $ctx, $evt ) = create_circ_ctx( %$params,
455 requestor => $requestor,
457 #fetch_patron_circ_summary => 1,
458 fetch_copy_statuses => 1,
459 fetch_copy_locations => 1,
465 my $copy = $ctx->{copy};
467 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
468 return OpenILS::Event->new('COPY_IN_TRANSIT')
469 if $stat == $U->copy_status_from_name('in transit')->id;
472 $ctx->{authtoken} = $authtoken;
475 if( $ctx->{copy} and ($evt = _handle_claims_returned($ctx)) ) {
476 return $evt unless $U->event_equals($evt, 'SUCCESS');
484 # no claims returned circ was found, check if there is any open circ
485 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
486 ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
487 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
492 $ctx->{permit_key} = _cache_permit_key();
493 my $events = _run_permit_scripts($ctx);
496 $evt = override_events($requestor, $requestor->ws_ou,
497 $events, $authtoken, $ctx->{copy}->id, $client);
499 return OpenILS::Event->new('SUCCESS', payload => $ctx->{permit_key} );
505 sub override_events {
507 my( $requestor, $org, $events, $authtoken, $copyid, $conn ) = @_;
508 $events = [ $events ] unless ref($events) eq 'ARRAY';
511 for my $e (@$events) {
512 my $tc = $e->{textcode};
513 next if $tc eq 'SUCCESS';
514 my $ov = "$tc.override";
515 $logger->info("attempting to override event $ov");
516 my $evt = $U->check_perms( $requestor->id, $org, $ov );
526 # Runs the patron and copy permit scripts
527 # if this is a non-cat circulation, the copy permit script
529 sub _run_permit_scripts {
532 my $runner = $ctx->{runner};
533 my $patronid = $ctx->{patron}->id;
534 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
535 my $key = $ctx->{permit_key};
538 # ---------------------------------------------------------------------
539 # Find all of the fatal penalties currently set on the user
540 # ---------------------------------------------------------------------
541 my $penalties = $U->update_patron_penalties(
542 authtoken => $ctx->{authtoken},
543 patron => $ctx->{patron}
546 $penalties = $penalties->{fatal_penalties};
547 $logger->info("circ patron penalties user $patronid: @$penalties");
550 # ---------------------------------------------------------------------
551 # Now run the patron permit script
552 # ---------------------------------------------------------------------
553 $logger->debug("Running circ script: " . $scripts{circ_permit_patron});
555 #$runner->load($scripts{circ_permit_patron});
556 $runner->run($scripts{circ_permit_patron}) or
557 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
559 my $patron_events = $runner->retrieve('result.events');
560 $patron_events = [ split(/,/, $patron_events) ];
561 $ctx->{circ_permit_patron_events} = $patron_events;
562 $logger->activity("circ_permit_patron for returned @$patron_events") if @$patron_events;
564 my @evts_so_far = (@$penalties, @$patron_events);
566 push( @allevents, OpenILS::Event->new($_)) for @evts_so_far;
568 return \@allevents if @allevents;
571 if( $ctx->{noncat} ) {
572 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
573 return OpenILS::Event->new('SUCCESS', payload => $key);
577 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
578 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
582 $logger->debug("Exiting circ permit early because request is for hold patron permit");
583 return OpenILS::Event->new('SUCCESS');
588 # ---------------------------------------------------------------------
589 # Capture all of the copy permit events
590 # ---------------------------------------------------------------------
591 $runner->load($scripts{circ_permit_copy});
592 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
594 my $copy_events = $runner->retrieve('result.events');
595 $copy_events = [ split(/,/, $copy_events) ];
596 $ctx->{circ_permit_copy_events} = $copy_events;
597 $logger->activity("circ_permit_copy for copy ".
598 "$barcode returned events: @$copy_events") if @$copy_events;
603 # ---------------------------------------------------------------------
604 # Now collect all of the events together
605 # ---------------------------------------------------------------------
607 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
609 my $ae = _check_copy_alert($ctx->{copy});
610 push( @allevents, $ae ) if $ae;
612 return OpenILS::Event->new('SUCCESS', payload => $key) unless (@allevents);
615 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
616 @allevents = values %hash;
619 $_->{payload} = $ctx->{copy}->status->id
620 if ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
626 sub _check_copy_alert {
628 return OpenILS::Event->new('COPY_ALERT_MESSAGE',
629 payload => $copy->alert_message) if $copy->alert_message;
633 # takes copyid, patronid, and requestor id
634 sub _cache_permit_key {
635 my $key = md5_hex( time() . rand() . "$$" );
636 $logger->debug("Setting circ permit key to $key");
637 $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
641 sub _check_permit_key {
643 $logger->debug("Fetching circ permit key $key");
644 my $k = "oils_permit_key_$key";
645 my $one = $cache_handle->get_cache($k);
646 $cache_handle->delete_cache($k);
647 return ($one) ? 1 : 0;
651 # ------------------------------------------------------------------------------
653 __PACKAGE__->register_method(
654 method => "checkout",
655 api_name => "open-ils.circ.checkout",
658 @param authtoken The login session key
659 @param params A named hash of params including:
661 barcode If no copy is provided, the copy is retrieved via barcode
662 copyid If no copy or barcode is provide, the copy id will be use
663 patron The patron's id
664 noncat True if this is a circulation for a non-cataloted item
665 noncat_type The non-cataloged type id
666 noncat_circ_lib The location for the noncat circ.
667 precat The item has yet to be cataloged
668 dummy_title The temporary title of the pre-cataloded item
669 dummy_author The temporary authr of the pre-cataloded item
670 Default is the home org of the staff member
671 @return The SUCCESS event on success, any other event depending on the error
675 my( $self, $client, $authtoken, $params ) = @_;
678 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
679 my $key = $params->{permit_key};
681 # if this is a renewal, then the requestor does not have to
682 # have checkout privelages
683 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
684 ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
687 if( $params->{patron} ) {
688 ( $patron, $evt ) = $U->fetch_user($params->{patron});
691 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
695 # set the circ lib to the home org of the requestor if not specified
696 my $circlib = (defined($params->{circ_lib})) ?
697 $params->{circ_lib} : $requestor->ws_ou;
700 # Make sure the caller has a valid permit key or is
701 # overriding the permit can
702 if( $params->{permit_override} ) {
703 $evt = $U->check_perms(
704 $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
708 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
709 unless _check_permit_key($key);
712 # if this is a non-cataloged item, check it out and return
713 return _checkout_noncat(
714 $key, $requestor, $patron, %$params ) if $params->{noncat};
716 # if this item has yet to be cataloged, make sure a dummy copy exists
717 ( $params->{copy}, $evt ) = _make_precat_copy(
718 $requestor, $circlib, $params ) if $params->{precat};
722 # fetch and build the circulation environment
723 if( !( $ctx = $params->{_ctx}) ) {
724 ( $ctx, $evt ) = create_circ_ctx( %$params,
726 requestor => $requestor,
727 session => $U->start_db_session(),
729 fetch_copy_statuses => 1,
730 fetch_copy_locations => 1,
734 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
736 # if the call doesn't know it's not cataloged..
737 if(!$params->{precat}) {
738 if( $ctx->{copy}->call_number eq '-1' ) {
739 return OpenILS::Event->new('ITEM_NOT_CATALOGED');
744 $copy = $ctx->{copy};
746 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
747 return OpenILS::Event->new('COPY_IN_TRANSIT')
748 if $stat == $U->copy_status_from_name('in transit')->id;
751 # this happens in permit.. but we need to check here for 'offline' requests
752 ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
753 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
755 my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
758 $ctx->{circ_lib} = $circlib;
760 $evt = _run_checkout_scripts($ctx);
764 _build_checkout_circ_object($ctx);
766 $evt = _apply_modified_due_date($ctx);
769 $evt = _commit_checkout_circ_object($ctx);
772 $evt = _update_checkout_copy($ctx);
776 ($holds, $evt) = _handle_related_holds($ctx);
780 $logger->debug("Checkout committing objects with session thread trace: ".$ctx->{session}->session_id);
781 $U->commit_db_session($ctx->{session});
782 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
784 $logger->activity("user ".$requestor->id." successfully checked out item ".
785 $ctx->{copy}->barcode." to user ".$ctx->{patron}->id );
788 # ------------------------------------------------------------------------------
789 # Update the patron penalty info in the DB
790 # ------------------------------------------------------------------------------
791 $U->update_patron_penalties(
792 authtoken => $authtoken,
793 patron => $ctx->{patron} ,
797 return OpenILS::Event->new('SUCCESS',
799 copy => $U->unflesh_copy($ctx->{copy}),
800 circ => $ctx->{circ},
802 holds_fulfilled => $holds,
808 sub _make_precat_copy {
809 my ( $requestor, $circlib, $params ) = @_;
811 my( $copy, undef ) = _find_copy_by_attr(%$params);
814 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
816 $copy->editor($requestor->id);
817 $copy->edit_date('now');
818 $copy->dummy_title($params->{dummy_title});
819 $copy->dummy_author($params->{dummy_author});
821 my $stat = $U->storagereq(
822 'open-ils.storage.direct.asset.copy.update', $copy );
824 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $stat;
828 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
830 my $evt = OpenILS::Event->new(
831 'BAD_PARAMS', desc => "Dummy title or author not provided" )
832 unless ( $params->{dummy_title} and $params->{dummy_author} );
833 return (undef, $evt) if $evt;
835 $copy = Fieldmapper::asset::copy->new;
836 $copy->circ_lib($circlib);
837 $copy->creator($requestor->id);
838 $copy->editor($requestor->id);
839 $copy->barcode($params->{barcode});
840 $copy->call_number(-1); #special CN for precat materials
841 $copy->loan_duration(&PRECAT_LOAN_DURATION);
842 $copy->fine_level(&PRECAT_FINE_LEVEL);
844 $copy->dummy_title($params->{dummy_title});
845 $copy->dummy_author($params->{dummy_author});
847 my $id = $U->storagereq(
848 'open-ils.storage.direct.asset.copy.create', $copy );
849 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
851 $logger->debug("Pre-cataloged copy successfully created");
852 return ($U->fetch_copy($id));
856 sub _run_checkout_scripts {
862 my $runner = $ctx->{runner};
864 $runner->insert('result.durationLevel');
865 $runner->insert('result.durationRule');
866 $runner->insert('result.recurringFinesRule');
867 $runner->insert('result.recurringFinesLevel');
868 $runner->insert('result.maxFine');
870 $runner->load($scripts{circ_duration});
871 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
872 my $duration = $runner->retrieve('result.durationRule');
873 $logger->debug("Circ duration script yielded a duration rule of: $duration");
875 $runner->load($scripts{circ_recurring_fines});
876 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
877 my $recurring = $runner->retrieve('result.recurringFinesRule');
878 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
880 $runner->load($scripts{circ_max_fines});
881 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
882 my $max_fine = $runner->retrieve('result.maxFine');
883 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
885 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
887 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
889 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
892 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
893 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
894 $ctx->{duration_rule} = $duration;
895 $ctx->{recurring_fines_rule} = $recurring;
896 $ctx->{max_fine_rule} = $max_fine;
901 sub _build_checkout_circ_object {
905 my $circ = new Fieldmapper::action::circulation;
906 my $duration = $ctx->{duration_rule};
907 my $max = $ctx->{max_fine_rule};
908 my $recurring = $ctx->{recurring_fines_rule};
909 my $copy = $ctx->{copy};
910 my $patron = $ctx->{patron};
911 my $dur_level = $ctx->{duration_level};
912 my $rec_level = $ctx->{recurring_fines_level};
914 $circ->duration( $duration->shrt ) if ($dur_level == 1);
915 $circ->duration( $duration->normal ) if ($dur_level == 2);
916 $circ->duration( $duration->extended ) if ($dur_level == 3);
918 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
919 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
920 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
922 $circ->duration_rule( $duration->name );
923 $circ->recuring_fine_rule( $recurring->name );
924 $circ->max_fine_rule( $max->name );
925 $circ->max_fine( $max->amount );
927 $circ->fine_interval($recurring->recurance_interval);
928 $circ->renewal_remaining( $duration->max_renewals );
929 $circ->target_copy( $copy->id );
930 $circ->usr( $patron->id );
931 $circ->circ_lib( $ctx->{circ_lib} );
934 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
935 $circ->opac_renewal(1);
936 $circ->renewal_remaining($ctx->{renewal_remaining});
937 $circ->circ_staff($ctx->{requestor}->id);
941 # if the user provided an overiding checkout time,
942 # (e.g. the checkout really happened several hours ago), then
943 # we apply that here. Does this need a perm??
944 if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
945 $logger->debug("circ setting checkout_time to $ds");
946 $circ->xact_start($ds);
949 # if a patron is renewing, 'requestor' will be the patron
950 $circ->circ_staff($ctx->{requestor}->id );
951 _set_circ_due_date($circ);
952 $ctx->{circ} = $circ;
955 sub _apply_modified_due_date {
957 my $circ = $ctx->{circ};
961 if( $ctx->{due_date} ) {
963 my $evt = $U->check_perms(
964 $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
967 my $ds = _create_date_stamp($ctx->{due_date});
968 $logger->debug("circ modifying due_date to $ds");
969 $circ->due_date($ds);
973 # if the due_date lands on a day when the location is closed
974 my $copy = $ctx->{copy};
977 $logger->info("circ searching for closed date overlap on lib ".
978 $copy->circ_lib->id ." with an item due date of ".$circ->due_date );
980 my $dateinfo = $ctx->{session}->request(
981 'open-ils.storage.actor.org_unit.closed_date.overlap',
982 $copy->circ_lib->id, $circ->due_date )->gather(1);
986 $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
987 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
989 # XXX make the behavior more dynamic
990 # for now, we just push the due date to after the close date
991 $circ->due_date($dateinfo->{end});
998 sub _create_date_stamp {
999 my $datestring = shift;
1000 return undef unless $datestring;
1001 $datestring = clense_ISO8601($datestring);
1002 $logger->debug("circ created date stamp => $datestring");
1006 sub _create_due_date {
1007 my $duration = shift;
1009 my ($sec,$min,$hour,$mday,$mon,$year) =
1010 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1011 $year += 1900; $mon += 1;
1012 my $due_date = sprintf(
1013 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1014 $year, $mon, $mday, $hour, $min, $sec);
1018 sub _set_circ_due_date {
1021 my $dd = _create_due_date($circ->duration);
1022 $logger->debug("Checkout setting due date on circ to: $dd");
1023 $circ->due_date($dd);
1026 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
1027 sub _update_checkout_copy {
1030 my $copy = $ctx->{copy};
1032 my $s = $U->copy_status_from_name('checked out');
1033 $copy->status( $s->id ) if $s;
1035 my $evt = $U->update_copy( session => $ctx->{session},
1036 copy => $copy, editor => $ctx->{requestor}->id );
1037 return (undef,$evt) if $evt;
1042 # commits the circ object to the db then fleshes the circ with rules objects
1043 sub _commit_checkout_circ_object {
1046 my $circ = $ctx->{circ};
1050 my $r = $ctx->{session}->request(
1051 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
1053 return $U->DB_UPDATE_FAILED($circ) unless $r;
1055 $logger->debug("Created a new circ object in checkout: $r");
1058 $circ->duration_rule($ctx->{duration_rule});
1059 $circ->max_fine_rule($ctx->{max_fine_rule});
1060 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
1066 # sees if there are any holds that this copy
1067 sub _handle_related_holds {
1070 my $copy = $ctx->{copy};
1071 my $patron = $ctx->{patron};
1072 my $holds = $holdcode->fetch_related_holds($copy->id);
1076 # XXX We should only fulfill one hold here...
1077 # XXX If a hold was transited to the user who is checking out
1078 # the item, we need to make sure that hold is what's grabbed
1079 if(ref($holds) && @$holds) {
1081 # for now, just sort by id to get what should be the oldest hold
1082 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1083 my @myholds = grep { $_->usr eq $patron->id } @$holds;
1084 my @altholds = grep { $_->usr ne $patron->id } @$holds;
1087 my $hold = $myholds[0];
1089 $logger->debug("Related hold found in checkout: " . $hold->id );
1091 $hold->current_copy($copy->id); # just make sure it's set
1092 # if the hold was never officially captured, capture it.
1093 $hold->capture_time('now') unless $hold->capture_time;
1094 $hold->fulfillment_time('now');
1095 my $r = $ctx->{session}->request(
1096 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
1097 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
1098 push( @fulfilled, $hold->id );
1101 # If there are any holds placed for other users that point to this copy,
1102 # then we need to un-target those holds so the targeter can pick a new copy
1105 $logger->info("Un-targeting hold ".$_->id.
1106 " because copy ".$copy->id." is getting checked out");
1108 $_->clear_current_copy;
1109 my $r = $ctx->{session}->request(
1110 "open-ils.storage.direct.action.hold_request.update", $_ )->gather(1);
1111 return (undef,$U->DB_UPDATE_FAILED( $_ )) unless $r;
1115 return (\@fulfilled, undef);
1118 sub _checkout_noncat {
1119 my ( $key, $requestor, $patron, %params ) = @_;
1120 my( $circ, $circlib, $evt );
1123 $circlib = $params{noncat_circ_lib} || $requestor->ws_ou;
1125 my $count = $params{noncat_count} || 1;
1126 my $cotime = _create_date_stamp($params{checkout_time}) || "";
1127 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1129 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1130 $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
1131 return $evt if $evt;
1134 return OpenILS::Event->new(
1135 'SUCCESS', payload => { noncat_circ => $circ } );
1139 __PACKAGE__->register_method(
1140 method => "generic_receive",
1141 api_name => "open-ils.circ.checkin",
1144 Generic super-method for handling all copies
1145 @param authtoken The login session key
1146 @param params Hash of named parameters including:
1147 barcode - The copy barcode
1148 force - If true, copies in bad statuses will be checked in and give good statuses
1153 __PACKAGE__->register_method(
1154 method => "generic_receive",
1155 api_name => "open-ils.circ.checkin.override",
1156 signature => q/@see open-ils.circ.checkin/
1159 sub generic_receive {
1160 my( $self, $conn, $authtoken, $params ) = @_;
1161 my( $ctx, $requestor, $evt );
1163 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1164 ( $requestor, $evt ) = $U->checksesperm(
1165 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1166 return $evt if $evt;
1169 my ($patron) = _find_patron_from_params($params);
1170 $ctx->{patron} = $patron if $patron;
1172 # load up the circ objects
1173 if( !( $ctx = $params->{_ctx}) ) {
1174 ( $ctx, $evt ) = create_circ_ctx( %$params,
1175 requestor => $requestor,
1176 session => $U->start_db_session(),
1178 fetch_copy_statuses => 1,
1179 fetch_copy_locations => 1,
1182 return $evt if $evt;
1184 $ctx->{override} = 1 if $self->api_name =~ /override/o;
1185 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1186 $ctx->{authtoken} = $authtoken;
1187 my $session = $ctx->{session};
1189 my $copy = $ctx->{copy};
1190 $U->unflesh_copy($copy);
1191 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $copy;
1193 $logger->info("Checkin copy called by user ".
1194 $requestor->id." for copy ".$copy->id);
1197 my $val = $self->checkin_do_receive($conn, $ctx);
1199 # ------------------------------------------------------------------------------
1200 # Update the patron penalty info in the DB
1201 # ------------------------------------------------------------------------------
1202 $U->update_patron_penalties(
1203 authtoken => $authtoken,
1204 patron => $ctx->{patron},
1211 sub checkin_do_receive {
1213 my( $self, $connection, $ctx ) = @_;
1216 my $copy = $ctx->{copy};
1217 my $session = $ctx->{session};
1218 my $requestor = $ctx->{requestor};
1219 my $change = 0; # did we actually do anything?
1224 # does the copy have an attached alert message?
1225 my $ae = _check_copy_alert($copy);
1226 push(@eventlist, $ae) if $ae;
1228 # is the copy is an a status we can't automatically resolve?
1229 $evt = _checkin_check_copy_status($ctx);
1230 push( @eventlist, $evt ) if $evt;
1233 # - see if the copy has an open circ attached
1234 #($ctx->{circ}, $evt) = $U->fetch_open_circulation($copy->id);
1235 ($ctx->{circ}, $evt) = $U->fetch_all_open_circulation($copy->id); # - get ones with stop fines as well
1236 return $evt if ($evt and $__isrenewal); # renewals require a circulation
1238 $circ = $ctx->{circ};
1240 # if the circ is marked as 'claims returned', add the event to the list
1241 push( @eventlist, OpenILS::Event->new('CIRC_CLAIMS_RETURNED') )
1242 if ($circ and $circ->stop_fines and $circ->stop_fines eq 'CLAIMSRETURNED');
1246 if($ctx->{override}) {
1247 $evt = override_events($requestor, $requestor->ws_ou, \@eventlist );
1248 return $evt if $evt;
1254 ($ctx->{transit}) = $U->fetch_open_transit_by_copy($copy->id);
1256 if( $ctx->{circ} ) {
1258 # There is an open circ on this item, close it out.
1260 $evt = _checkin_handle_circ($ctx);
1261 return $evt if $evt;
1263 } elsif( $ctx->{transit} ) {
1265 # is this item currently in transit?
1267 $evt = $transcode->transit_receive( $copy, $requestor, $session );
1268 my $holdtrans = $evt->{holdtransit};
1269 ($ctx->{hold}) = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1271 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1273 # either an error occurred or a ROUTE_ITEM was generated and the
1274 # item must be forwarded on to its destination.
1275 return _checkin_flesh_event($ctx, $evt);
1279 # Transit has been closed, now let's see if the copy's original
1280 # status is something the staff should be warned of
1281 my $e = _checkin_check_copy_status($ctx);
1286 # copy was received as a hold transit. Copy is at target lib
1287 # and hold transit is complete. We're done here...
1288 $U->commit_db_session($session);
1289 return _checkin_flesh_event($ctx, $evt);
1295 # ------------------------------------------------------------------------------
1296 # Circulations and transits are now closed where necessary. Now go on to see if
1297 # this copy can fulfill a hold or needs to be routed to a different location
1298 # ------------------------------------------------------------------------------
1301 # If it's a renewal, we're done
1303 $U->commit_db_session($session);
1304 return OpenILS::Event->new('SUCCESS');
1307 # Now, let's see if this copy is needed for a hold
1308 my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor );
1312 $ctx->{hold} = $hold;
1315 # Capture the hold with this copy
1316 return $evt if ($evt = _checkin_capture_hold($ctx));
1318 if( $hold->pickup_lib == $requestor->ws_ou ) {
1320 # This hold was captured in the correct location
1321 $evt = OpenILS::Event->new('SUCCESS');
1325 # Hold needs to be picked up elsewhere. Build a hold
1326 # transit and route the item.
1327 return $evt if ($evt =_checkin_build_hold_transit($ctx));
1328 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1331 } else { # not needed for a hold
1333 if( $copy->circ_lib == $requestor->ws_ou ) {
1335 # Copy is in the right place.
1336 $evt = OpenILS::Event->new('SUCCESS');
1338 # if the item happens to be a pre-cataloged item, send it
1339 # to cataloging and return the event
1340 my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1341 return $err if $err;
1347 # Copy wants to go home. Transit it there.
1348 return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1349 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1355 # ------------------------------------------------------------------
1356 # if the copy is not in a state that should persist,
1357 # set the copy to reshelving if it's not already there
1358 # ------------------------------------------------------------------
1359 my ($c, $e) = _reshelve_copy($ctx);
1361 $change = $c unless $change;
1365 $evt = OpenILS::Event->new('NO_CHANGE');
1366 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id)
1369 if( $copy->status == $U->copy_status_from_name('on holds shelf')->id );
1373 $U->commit_db_session($session);
1376 $logger->activity("checkin by user ".$requestor->id." on item ".
1377 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1379 return _checkin_flesh_event($ctx, $evt);
1382 sub _reshelve_copy {
1385 my $copy = $ctx->{copy};
1386 my $reqr = $ctx->{requestor};
1387 my $session = $ctx->{session};
1388 my $force = $ctx->{force};
1390 my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1393 $stat != $U->copy_status_from_name('on holds shelf')->id and
1394 $stat != $U->copy_status_from_name('available')->id and
1395 $stat != $U->copy_status_from_name('cataloging')->id and
1396 $stat != $U->copy_status_from_name('in transit')->id and
1397 $stat != $U->copy_status_from_name('reshelving')->id) ) {
1399 $copy->status( $U->copy_status_from_name('reshelving')->id );
1401 my $evt = $U->update_copy(
1403 editor => $reqr->id,
1404 session => $session,
1415 # returns undef if there are no 'open' claims-returned circs attached
1416 # to the given copy. if there is an open claims-returned circ,
1417 # then we check for override mode. if in override, mark the claims-returned
1418 # circ as checked in. if not, return event.
1419 sub _handle_claims_returned {
1421 my $copy = $ctx->{copy};
1423 my $CR = _fetch_open_claims_returned($copy->id);
1424 return undef unless $CR;
1426 # - If the caller has set the override flag, we will check the item in
1427 if($ctx->{override}) {
1429 $CR->checkin_time('now');
1430 $CR->checkin_lib($ctx->{requestor}->ws_ou);
1431 $CR->checkin_staff($ctx->{requestor}->id);
1433 my $stat = $U->storagereq(
1434 'open-ils.storage.direct.action.circulation.update', $CR);
1435 return $U->DB_UPDATE_FAILED($CR) unless $stat;
1436 return OpenILS::Event->new('SUCCESS');
1439 # - if not in override mode, return the CR event
1440 return OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1445 sub _fetch_open_claims_returned {
1447 my $trans = $U->storagereq(
1448 'open-ils.storage.direct.action.circulation.search_where',
1450 target_copy => $copyid,
1451 stop_fines => 'CLAIMSRETURNED',
1452 checkin_time => undef,
1455 return $$trans[0] if $trans && $$trans[0];
1460 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1461 sub _checkin_handle_precat {
1464 my $copy = $ctx->{copy};
1469 my $catstat = $U->copy_status_from_name('cataloging');
1471 if( $ctx->{precat} ) {
1473 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1475 if( $copy->status != $catstat->id ) {
1476 $copy->status($catstat->id);
1478 return (undef, 0, $errevt) if (
1479 $errevt = $U->update_copy(
1481 editor => $ctx->{requestor}->id,
1482 session => $ctx->{session} ));
1488 return ($evt, $change, undef);
1492 # returns the appropriate event for the given copy status
1493 # if the copy is not in a 'special' status, undef is returned
1494 sub _checkin_check_copy_status {
1496 my $copy = $ctx->{copy};
1497 my $reqr = $ctx->{requestor};
1498 my $ses = $ctx->{session};
1504 my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1507 if( $status == $U->copy_status_from_name('available')->id ||
1508 $status == $U->copy_status_from_name('checked out')->id ||
1509 $status == $U->copy_status_from_name('in process')->id ||
1510 $status == $U->copy_status_from_name('in transit')->id ||
1511 $status == $U->copy_status_from_name('reshelving')->id );
1513 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1514 if( $status == $U->copy_status_from_name('lost')->id );
1516 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1517 if( $status == $U->copy_status_from_name('missing')->id );
1519 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
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 # backdate the circ if necessary
1641 if(my $backdate = $ctx->{backdate}) {
1642 return $evt if ($evt =
1643 _checkin_handle_backdate($backdate, $circ, $requestor, $session, 1));
1647 if(!$circ->stop_fines) {
1648 $circ->stop_fines('CHECKIN');
1649 $circ->stop_fines('RENEW') if $__isrenewal;
1650 $circ->stop_fines_time('now');
1653 # see if there are any fines owed on this circ. if not, close it
1654 ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1655 return $evt if $evt;
1656 $circ->xact_finish('now') if( $obt->balance_owed == 0 );
1658 # Set the checkin vars since we have the item
1659 $circ->checkin_time('now');
1660 $circ->checkin_staff($requestor->id);
1661 $circ->checkin_lib($requestor->ws_ou);
1663 $evt = _set_copy_reshelving($copy, $requestor->id, $ctx->{session});
1664 return $evt if $evt;
1666 $ctx->{session}->request(
1667 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1672 sub _set_copy_reshelving {
1673 my( $copy, $reqr, $session ) = @_;
1675 $logger->info("Setting copy ".$copy->id." to reshelving");
1676 $copy->status($U->copy_status_from_name('reshelving')->id);
1678 my $evt = $U->update_copy(
1679 session => $session,
1683 return $evt if $evt;
1686 # returns event on error, undef on success
1687 # This voids all bills attached to the given circulation that occurred
1688 # after the backdate
1689 # THIS DOES NOT CLOSE THE CIRC if there are no more fines on the item
1690 sub _checkin_handle_backdate {
1691 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1693 $logger->activity("User ".$requestor->id.
1694 " backdating circ [".$circ->target_copy."] to date: $backdate");
1696 my $bills = $session->request( # XXX Verify this call is correct
1697 "open-ils.storage.direct.money.billing.search_where.atomic",
1698 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1701 for my $bill (@$bills) {
1703 my $n = $bill->note || "";
1704 $bill->note($n . "\nSYSTEM VOIDED FOR BACKDATE");
1705 my $s = $session->request(
1706 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1707 return $U->DB_UPDATE_FAILED($bill) unless $s;
1711 # if the caller elects to attempt to close the circulation
1712 # transaction, then it will be closed if there are not further
1713 # charges on the transaction
1715 #my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1716 #return $evt if $evt;
1717 #$circ->xact_finish($backdate) if $obt->balance_owed <= 0;
1724 sub _find_patron_from_params {
1732 if(my $barcode = $params->{barcode}) {
1733 $logger->debug("circ finding user from params with barcode $barcode");
1734 ($copy, $evt) = $U->fetch_copy_by_barcode($barcode);
1735 return (undef, undef, $evt) if $evt;
1736 ($circ, $evt) = $U->fetch_open_circulation($copy->id);
1737 return (undef, undef, $evt) if $evt;
1738 ($patron, $evt) = $U->fetch_user($circ->usr);
1739 return (undef, undef, $evt) if $evt;
1741 return ($patron, $copy);
1745 # ------------------------------------------------------------------------------
1747 __PACKAGE__->register_method(
1749 api_name => "open-ils.circ.renew.override",
1750 signature => q/@see open-ils.circ.renew/,
1754 __PACKAGE__->register_method(
1756 api_name => "open-ils.circ.renew",
1757 notes => <<" NOTES");
1758 PARAMS( authtoken, circ => circ_id );
1759 open-ils.circ.renew(login_session, circ_object);
1760 Renews the provided circulation. login_session is the requestor of the
1761 renewal and if the logged in user is not the same as circ->usr, then
1762 the logged in user must have RENEW_CIRC permissions.
1766 my( $self, $client, $authtoken, $params ) = @_;
1769 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1772 $params->{override} = 1 if $self->api_name =~ /override/o;
1774 # fetch the patron object one way or another
1775 if( $params->{patron} ) {
1776 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1777 if($evt) { $__isrenewal = 0; return $evt; }
1779 } elsif( $params->{patron_barcode} ) {
1780 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1781 if($evt) { $__isrenewal = 0; return $evt; }
1784 ($patron, $copy, $evt) = _find_patron_from_params($params);
1785 if($evt) { $__isrenewal = 0; return $evt; }
1786 $params->{copy} = $copy;
1789 # verify our login session
1790 ($requestor, $evt) = $U->checkses($authtoken);
1791 if($evt) { $__isrenewal = 0; return $evt; }
1793 # make sure we have permission to perform a renewal
1794 if( $requestor->id ne $patron->id ) {
1795 $evt = $U->check_perms($requestor->id, $requestor->ws_ou, 'RENEW_CIRC');
1796 if($evt) { $__isrenewal = 0; return $evt; }
1800 # fetch and build the circulation environment
1801 ( $ctx, $evt ) = create_circ_ctx( %$params,
1803 requestor => $requestor,
1806 fetch_copy_statuses => 1,
1807 fetch_copy_locations => 1,
1809 if($evt) { $__isrenewal = 0; return $evt; }
1810 $params->{_ctx} = $ctx;
1812 # make sure they have some renewals left and make sure the circulation exists
1813 ($circ, $evt) = _check_renewal_remaining($ctx);
1814 if($evt) { $__isrenewal = 0; return $evt; }
1815 $ctx->{old_circ} = $circ;
1816 my $renewals = $circ->renewal_remaining - 1;
1818 # run the renew permit script
1819 $evt = _run_renew_scripts($ctx);
1820 if($evt) { $__isrenewal = 0; return $evt; }
1823 #$ctx->{patron} = $ctx->{patron}->id;
1824 $evt = $self->generic_receive($client, $authtoken, $ctx );
1825 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1827 if( !$U->event_equals($evt, 'SUCCESS') ) {
1828 $__isrenewal = 0; return $evt;
1831 # re-fetch the context since objects have changed in the checkin
1832 # XXX Do we really need to do this - what changes that we don't control??
1833 ( $ctx, $evt ) = create_circ_ctx( %$params,
1835 requestor => $requestor,
1838 fetch_copy_statuses => 1,
1839 fetch_copy_locations => 1,
1841 if($evt) { $__isrenewal = 0; return $evt; }
1842 $params->{_ctx} = $ctx;
1843 $ctx->{renewal_remaining} = $renewals;
1845 # run the circ permit scripts
1846 if( $ctx->{permit_override} ) {
1847 $evt = $U->check_perms(
1848 $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1849 if($evt) { $__isrenewal = 0; return $evt; }
1852 $evt = $self->permit_circ( $client, $authtoken, $params );
1853 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1854 $params->{precat} = 1;
1857 if(!$U->event_equals($evt, 'SUCCESS')) {
1858 if($evt) { $__isrenewal = 0; return $evt; }
1861 $params->{permit_key} = $evt->{payload};
1865 # checkout the item again
1866 $params->{patron} = $ctx->{patron}->id;
1867 $evt = $self->checkout($client, $authtoken, $params );
1869 $logger->activity("user ".$requestor->id." renewl of item ".
1870 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1876 sub _check_renewal_remaining {
1879 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1880 return (undef, $evt) if $evt;
1881 $evt = OpenILS::Event->new(
1882 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1883 return ($circ, $evt);
1886 sub _run_renew_scripts {
1888 my $runner = $ctx->{runner};
1891 $runner->load($scripts{circ_permit_renew});
1892 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1894 my $events = $runner->retrieve('result.events');
1895 $events = [ split(/,/, $events) ];
1896 $logger->activity("circ_permit_renew for user ".
1897 $ctx->{patron}->id." returned events: @$events") if @$events;
1900 push( @allevents, OpenILS::Event->new($_)) for @$events;
1901 return \@allevents if @allevents;