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 OpenILS::Application::Circ::ScriptBuilder->build($ctx);
103 my @evts = @{$ctx->{_events}} if $ctx->{_events};
105 if(!$params{noncat}) {
106 if( @evts and grep { $_->{textcode} eq 'ASSET_COPY_NOT_FOUND' } @evts) {
109 $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
113 warn "PRECAT = TRUE\n" if $ctx->{precat};
115 _build_circ_script_runner($ctx);
120 # $evt = _ctx_add_patron_objects($ctx, %params);
121 # return (undef,$evt) if $evt;
123 # if(!$params{noncat}) {
124 # if( $evt = _ctx_add_copy_objects($ctx, %params) ) {
125 # $ctx->{precat} = 1 if($evt->{textcode} eq 'ASSET_COPY_NOT_FOUND')
127 # $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
131 # _doctor_patron_object($ctx) if $ctx->{patron};
132 # _doctor_copy_object($ctx) if $ctx->{copy};
134 # if(!$ctx->{no_runner}) {
135 # _build_circ_script_runner($ctx);
136 # _add_script_runner_methods($ctx);
143 #sub _ctx_add_patron_objects {
144 # my( $ctx, %params) = @_;
147 # $cache{group_tree} = $U->fetch_permission_group_tree() unless $cache{group_tree};
148 # $ctx->{group_tree} = $cache{group_tree};
150 # $ctx->{patron_circ_summary} =
151 # $U->fetch_patron_circ_summary($ctx->{patron}->id)
152 # if $params{fetch_patron_circsummary};
158 sub _find_copy_by_attr {
163 my $copy = $params{copy} || undef;
168 $U->fetch_copy($params{copyid}) if $params{copyid};
169 return (undef,$evt) if $evt;
173 $U->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
174 return (undef,$evt) if $evt;
177 return ( $copy, $evt );
181 #sub _ctx_add_copy_objects {
182 # my($ctx, %params) = @_;
187 # $cache{copy_statuses} = $U->fetch_copy_statuses
188 # if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
190 # $cache{copy_locations} = $U->fetch_copy_locations
191 # if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
193 # $ctx->{copy_statuses} = $cache{copy_statuses};
194 # $ctx->{copy_locations} = $cache{copy_locations};
196 # ($copy, $evt) = _find_copy_by_attr(%params);
197 # return $evt if $evt;
199 # if( $copy and !$ctx->{title} ) {
201 # my $r = $RECORD_FROM_COPY_CACHE{$copy->id};
202 # ($r, $evt) = $U->fetch_record_by_copy( $copy->id ) unless $r;
203 # return $evt if $evt;
204 # $RECORD_FROM_COPY_CACHE{$copy->id} = $r;
206 # $ctx->{title} = $r;
207 # $ctx->{copy} = $copy;
209 # ($ctx->{volume}) = $U->fetch_callnumber($copy->call_number);
210 # $ctx->{recordDescriptor} = $U->storagereq(
211 # 'open-ils.storage.direct.metabib.record_descriptor.search_where',
212 # { record => $ctx->{title}->id });
221 ## ------------------------------------------------------------------------------
222 ## Fleshes parts of the patron object
223 ## ------------------------------------------------------------------------------
224 #sub _doctor_copy_object {
227 # my $copy = $ctx->{copy} || return undef;
229 # $logger->debug("Doctoring copy object...");
231 # # set the copy status to a status name
232 # $copy->status( _get_copy_status( $copy, $ctx->{copy_statuses} ) );
234 # # set the copy location to the location object
235 # $copy->location( _get_copy_location( $copy, $ctx->{copy_locations} ) );
237 # $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
242 ## ------------------------------------------------------------------------------
243 ## Fleshes parts of the patron object
244 ## ------------------------------------------------------------------------------
245 #sub _doctor_patron_object {
248 # my $patron = $ctx->{patron} || return undef;
250 # # set the patron ptofile to the profile name
251 # $patron->profile( _get_patron_profile(
252 # $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
254 # # flesh the org unit
256 # $U->fetch_org_unit( $patron->home_ou ) ) if $patron;
260 ## recurse and find the patron profile name from the tree
261 ## another option would be to grab the groups for the patron
262 ## and cycle through those until the "profile" group has been found
263 #sub _get_patron_profile {
264 # my( $patron, $group_tree ) = @_;
265 # return $group_tree if ($group_tree->id eq $patron->profile);
266 # return undef unless ($group_tree->children);
268 # for my $child (@{$group_tree->children}) {
269 # my $ret = _get_patron_profile( $patron, $child );
270 # return $ret if $ret;
275 #sub _get_copy_status {
276 # my( $copy, $cstatus ) = @_;
279 # for my $status (@$cstatus) {
280 # $s = $status if( $status->id eq $copy->status )
282 # $logger->debug("Retrieving copy status: " . $s->name) if $s;
286 #sub _get_copy_location {
287 # my( $copy, $locations ) = @_;
290 # for my $loc (@$locations) {
291 # $l = $loc if $loc->id eq $copy->location;
293 # $logger->debug("Retrieving copy location: " . $l->name ) if $l;
298 # ------------------------------------------------------------------------------
299 # Constructs and shoves data into the script environment
300 # ------------------------------------------------------------------------------
301 sub _build_circ_script_runner {
305 $logger->debug("Loading script environment for circulation");
308 my $runner = $ctx->{runner};
311 $runner->insert('environment.isRenewal', 1);
313 $runner->insert('environment.isRenewal', undef);
316 if($ctx->{ishold} ) {
317 $runner->insert('environment.isHold', 1);
319 $runner->insert('environment.isHold', undef)
322 if( $ctx->{noncat} ) {
323 $runner->insert('environment.isNonCat', 1);
324 $runner->insert('environment.nonCatType', $ctx->{noncat_type});
326 $runner->insert('environment.isNonCat', undef);
330 $logger->debug("Loading circ script lib path $_");
331 $runner->add_path( $_ );
343 # for(@$script_libs) {
344 # $logger->debug("Loading circ script lib path $_");
345 # $runner->add_path( $_ );
348 # # Note: inserting the number 0 into the script turns into the
349 # # string "0", and thus evaluates to true in JS land
350 # # inserting undef will insert "", which evaluates to false
352 # $runner->insert( 'environment.patron', $ctx->{patron}, 1);
353 # $runner->insert( 'environment.record', $ctx->{title}, 1);
354 # $runner->insert( 'environment.copy', $ctx->{copy}, 1);
355 # $runner->insert( 'environment.volume', $ctx->{volume}, 1);
356 # $runner->insert( 'environment.recordDescriptor', $ctx->{recordDescriptor}, 1);
357 # $runner->insert( 'environment.requestor', $ctx->{requestor}, 1);
359 # # circ script result
360 # $runner->insert( 'result', {} );
361 # #$runner->insert( 'result.event', 'SUCCESS' );
362 # $runner->insert( 'result.events', [] );
365 # $runner->insert('environment.isRenewal', 1);
367 # $runner->insert('environment.isRenewal', undef);
370 # if($ctx->{ishold} ) {
371 # $runner->insert('environment.isHold', 1);
373 # $runner->insert('environment.isHold', undef)
376 # if( $ctx->{noncat} ) {
377 # $runner->insert('environment.isNonCat', 1);
378 # $runner->insert('environment.nonCatType', $ctx->{noncat_type});
380 # $runner->insert('environment.isNonCat', undef);
383 # if(ref($ctx->{patron_circ_summary})) {
384 # $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
387 # $ctx->{runner} = $runner;
398 #sub _add_script_runner_methods {
401 # my $runner = $ctx->{runner};
403 # if( $ctx->{copy} ) {
405 # # allows a script to fetch a hold that is currently targeting the
407 # $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
409 # my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
410 # $hold = undef unless $hold;
411 # $runner->insert( $key, $hold, 1 );
417 # ------------------------------------------------------------------------------
420 __PACKAGE__->register_method(
421 method => "permit_circ",
422 api_name => "open-ils.circ.checkout.permit",
424 Determines if the given checkout can occur
425 @param authtoken The login session key
426 @param params A trailing hash of named params including
427 barcode : The copy barcode,
428 patron : The patron the checkout is occurring for,
429 renew : true or false - whether or not this is a renewal
430 @return The event that occurred during the permit check.
433 __PACKAGE__->register_method (
434 method => 'permit_circ',
435 api_name => 'open-ils.circ.checkout.permit.override',
436 signature => q/@see open-ils.circ.checkout.permit/,
440 my( $self, $client, $authtoken, $params ) = @_;
443 my $override = $params->{override} = 1 if $self->api_name =~ /override/o;
445 my ( $requestor, $patron, $ctx, $evt, $circ );
447 # check permisson of the requestor
448 ( $requestor, $patron, $evt ) =
449 $U->checkses_requestor(
450 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
454 # fetch and build the circulation environment
455 if( !( $ctx = $params->{_ctx}) ) {
457 ( $ctx, $evt ) = create_circ_ctx( %$params,
459 requestor => $requestor,
461 #fetch_patron_circ_summary => 1,
462 fetch_copy_statuses => 1,
463 fetch_copy_locations => 1,
469 my $copy = $ctx->{copy};
471 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
472 return OpenILS::Event->new('COPY_IN_TRANSIT')
473 if $stat == $U->copy_status_from_name('in transit')->id;
476 $ctx->{authtoken} = $authtoken;
479 if( $ctx->{copy} and ($evt = _handle_claims_returned($ctx)) ) {
480 return $evt unless $U->event_equals($evt, 'SUCCESS');
488 # no claims returned circ was found, check if there is any open circ
489 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
490 ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
491 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
496 $ctx->{permit_key} = _cache_permit_key();
497 my $events = _run_permit_scripts($ctx);
500 $evt = override_events($requestor, $requestor->ws_ou,
501 $events, $authtoken, $client);
503 return OpenILS::Event->new(
504 'ITEM_NOT_CATALOGED', payload => $ctx->{permit_key}) if $ctx->{precat};
505 return OpenILS::Event->new('SUCCESS', payload => $ctx->{permit_key} );
511 sub override_events {
513 my( $requestor, $org, $events, $authtoken, $conn ) = @_;
514 $events = [ $events ] unless ref($events) eq 'ARRAY';
517 for my $e (@$events) {
518 my $tc = $e->{textcode};
519 next if $tc eq 'SUCCESS';
520 my $ov = "$tc.override";
521 $logger->info("attempting to override event $ov");
522 my $evt = $U->check_perms( $requestor->id, $org, $ov );
532 # Runs the patron and copy permit scripts
533 # if this is a non-cat circulation, the copy permit script
535 sub _run_permit_scripts {
538 my $runner = $ctx->{runner};
539 my $patronid = $ctx->{patron}->id;
540 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
541 my $key = $ctx->{permit_key};
544 # ---------------------------------------------------------------------
545 # Find all of the fatal penalties currently set on the user
546 # ---------------------------------------------------------------------
547 my $penalties = $U->update_patron_penalties(
548 authtoken => $ctx->{authtoken},
549 patron => $ctx->{patron}
552 $penalties = $penalties->{fatal_penalties};
553 $logger->info("circ patron penalties user $patronid: @$penalties");
556 # ---------------------------------------------------------------------
557 # Now run the patron permit script
558 # ---------------------------------------------------------------------
559 $logger->debug("Running circ script: " . $scripts{circ_permit_patron});
561 $runner->load($scripts{circ_permit_patron});
562 my $result = $runner->run or
563 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
565 my $patron_events = $result->{events};
566 $ctx->{circ_permit_patron_events} = $patron_events;
567 $logger->activity("circ_permit_patron for returned @$patron_events") if @$patron_events;
569 my @evts_so_far = (@$penalties, @$patron_events);
571 push( @allevents, OpenILS::Event->new($_)) for @evts_so_far;
574 return \@allevents if @allevents;
577 warn "Item is precat in checkout permit\n";
578 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
579 #push( @allevents, OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key));
580 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
583 if( $ctx->{noncat} ) {
584 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
585 return OpenILS::Event->new('SUCCESS', payload => $key);
590 $logger->debug("Exiting circ permit early because request is for hold patron permit");
591 return OpenILS::Event->new('SUCCESS');
596 # ---------------------------------------------------------------------
597 # Capture all of the copy permit events
598 # ---------------------------------------------------------------------
599 $runner->load($scripts{circ_permit_copy});
600 $result = $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
601 my $copy_events = $result->{events};
603 $ctx->{circ_permit_copy_events} = $copy_events;
604 $logger->activity("circ_permit_copy for copy ".
605 "$barcode returned events: @$copy_events") if @$copy_events;
610 # ---------------------------------------------------------------------
611 # Now collect all of the events together
612 # ---------------------------------------------------------------------
614 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
616 my $ae = _check_copy_alert($ctx->{copy});
617 push( @allevents, $ae ) if $ae;
619 return OpenILS::Event->new('SUCCESS', payload => $key) unless (@allevents);
622 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
623 @allevents = values %hash;
626 $_->{payload} = $ctx->{copy}->status->id
627 if ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
633 sub _check_copy_alert {
635 return OpenILS::Event->new('COPY_ALERT_MESSAGE',
636 payload => $copy->alert_message) if $copy->alert_message;
640 # takes copyid, patronid, and requestor id
641 sub _cache_permit_key {
642 my $key = md5_hex( time() . rand() . "$$" );
643 $logger->debug("Setting circ permit key to $key");
644 $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
648 sub _check_permit_key {
650 $logger->debug("Fetching circ permit key $key");
651 my $k = "oils_permit_key_$key";
652 my $one = $cache_handle->get_cache($k);
653 $cache_handle->delete_cache($k);
654 return ($one) ? 1 : 0;
658 # ------------------------------------------------------------------------------
660 __PACKAGE__->register_method(
661 method => "checkout",
662 api_name => "open-ils.circ.checkout",
665 @param authtoken The login session key
666 @param params A named hash of params including:
668 barcode If no copy is provided, the copy is retrieved via barcode
669 copyid If no copy or barcode is provide, the copy id will be use
670 patron The patron's id
671 noncat True if this is a circulation for a non-cataloted item
672 noncat_type The non-cataloged type id
673 noncat_circ_lib The location for the noncat circ.
674 precat The item has yet to be cataloged
675 dummy_title The temporary title of the pre-cataloded item
676 dummy_author The temporary authr of the pre-cataloded item
677 Default is the home org of the staff member
678 @return The SUCCESS event on success, any other event depending on the error
682 my( $self, $client, $authtoken, $params ) = @_;
685 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
686 my $key = $params->{permit_key};
688 # if this is a renewal, then the requestor does not have to
689 # have checkout privelages
690 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
691 ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
694 if( $params->{patron} ) {
695 ( $patron, $evt ) = $U->fetch_user($params->{patron});
698 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
702 # set the circ lib to the home org of the requestor if not specified
703 my $circlib = (defined($params->{circ_lib})) ?
704 $params->{circ_lib} : $requestor->ws_ou;
707 # Make sure the caller has a valid permit key or is
708 # overriding the permit can
709 if( $params->{permit_override} ) {
710 $evt = $U->check_perms(
711 $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
715 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
716 unless _check_permit_key($key);
719 # if this is a non-cataloged item, check it out and return
720 return _checkout_noncat(
721 $key, $requestor, $patron, %$params ) if $params->{noncat};
723 # if this item has yet to be cataloged, make sure a dummy copy exists
724 ( $params->{copy}, $evt ) = _make_precat_copy(
725 $requestor, $circlib, $params ) if $params->{precat};
729 # fetch and build the circulation environment
730 if( !( $ctx = $params->{_ctx}) ) {
731 ( $ctx, $evt ) = create_circ_ctx( %$params,
733 requestor => $requestor,
734 session => $U->start_db_session(),
736 fetch_copy_statuses => 1,
737 fetch_copy_locations => 1,
741 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
743 # if the call doesn't know it's not cataloged..
744 if(!$params->{precat}) {
745 if( $ctx->{copy}->call_number eq '-1' ) {
746 return OpenILS::Event->new('ITEM_NOT_CATALOGED');
751 $copy = $ctx->{copy};
753 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
754 return OpenILS::Event->new('COPY_IN_TRANSIT')
755 if $stat == $U->copy_status_from_name('in transit')->id;
758 # this happens in permit.. but we need to check here for 'offline' requests
759 ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
760 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
762 my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
765 $ctx->{circ_lib} = $circlib;
767 $evt = _run_checkout_scripts($ctx);
771 _build_checkout_circ_object($ctx);
773 $evt = _apply_modified_due_date($ctx);
776 $evt = _commit_checkout_circ_object($ctx);
779 $evt = _update_checkout_copy($ctx);
783 ($holds, $evt) = _handle_related_holds($ctx);
787 $logger->debug("Checkout committing objects with session thread trace: ".$ctx->{session}->session_id);
788 $U->commit_db_session($ctx->{session});
789 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
791 $logger->activity("user ".$requestor->id." successfully checked out item ".
792 $ctx->{copy}->barcode." to user ".$ctx->{patron}->id );
795 # ------------------------------------------------------------------------------
796 # Update the patron penalty info in the DB
797 # ------------------------------------------------------------------------------
798 $U->update_patron_penalties(
799 authtoken => $authtoken,
800 patron => $ctx->{patron} ,
804 return OpenILS::Event->new('SUCCESS',
806 copy => $U->unflesh_copy($ctx->{copy}),
807 circ => $ctx->{circ},
809 holds_fulfilled => $holds,
815 sub _make_precat_copy {
816 my ( $requestor, $circlib, $params ) = @_;
818 my( $copy, undef ) = _find_copy_by_attr(%$params);
821 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
823 $copy->editor($requestor->id);
824 $copy->edit_date('now');
825 $copy->dummy_title($params->{dummy_title});
826 $copy->dummy_author($params->{dummy_author});
828 my $stat = $U->storagereq(
829 'open-ils.storage.direct.asset.copy.update', $copy );
831 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $stat;
835 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
837 my $evt = OpenILS::Event->new(
838 'BAD_PARAMS', desc => "Dummy title or author not provided" )
839 unless ( $params->{dummy_title} and $params->{dummy_author} );
840 return (undef, $evt) if $evt;
842 $copy = Fieldmapper::asset::copy->new;
843 $copy->circ_lib($circlib);
844 $copy->creator($requestor->id);
845 $copy->editor($requestor->id);
846 $copy->barcode($params->{barcode});
847 $copy->call_number(-1); #special CN for precat materials
848 $copy->loan_duration(&PRECAT_LOAN_DURATION);
849 $copy->fine_level(&PRECAT_FINE_LEVEL);
851 $copy->dummy_title($params->{dummy_title});
852 $copy->dummy_author($params->{dummy_author});
854 my $id = $U->storagereq(
855 'open-ils.storage.direct.asset.copy.create', $copy );
856 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
858 $logger->debug("Pre-cataloged copy successfully created");
859 return ($U->fetch_copy($id));
863 sub _run_checkout_scripts {
869 my $runner = $ctx->{runner};
871 $runner->insert('result.durationLevel');
872 $runner->insert('result.durationRule');
873 $runner->insert('result.recurringFinesRule');
874 $runner->insert('result.recurringFinesLevel');
875 $runner->insert('result.maxFine');
877 $runner->load($scripts{circ_duration});
878 my $result = $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
879 my $duration = $result->{durationRule};
880 my $dur_level = $result->{durationLevel};
881 $logger->debug("Circ duration script yielded a duration rule of: $duration");
883 $runner->load($scripts{circ_recurring_fines});
884 $result = $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
885 my $recurring = $result->{recurringFinesRule};
886 my $rec_fines_level = $result->{recurringFinesLevel};
887 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
889 $runner->load($scripts{circ_max_fines});
890 $result = $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
891 my $max_fine = $result->{maxFine};
892 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
894 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
896 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
898 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
901 $ctx->{duration_level} = $dur_level;
902 $ctx->{recurring_fines_level} = $rec_fines_level;
903 $ctx->{duration_rule} = $duration;
904 $ctx->{recurring_fines_rule} = $recurring;
905 $ctx->{max_fine_rule} = $max_fine;
910 sub _build_checkout_circ_object {
914 my $circ = new Fieldmapper::action::circulation;
915 my $duration = $ctx->{duration_rule};
916 my $max = $ctx->{max_fine_rule};
917 my $recurring = $ctx->{recurring_fines_rule};
918 my $copy = $ctx->{copy};
919 my $patron = $ctx->{patron};
920 my $dur_level = $ctx->{duration_level};
921 my $rec_level = $ctx->{recurring_fines_level};
923 $circ->duration( $duration->shrt ) if ($dur_level == 1);
924 $circ->duration( $duration->normal ) if ($dur_level == 2);
925 $circ->duration( $duration->extended ) if ($dur_level == 3);
927 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
928 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
929 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
931 $circ->duration_rule( $duration->name );
932 $circ->recuring_fine_rule( $recurring->name );
933 $circ->max_fine_rule( $max->name );
934 $circ->max_fine( $max->amount );
936 $circ->fine_interval($recurring->recurance_interval);
937 $circ->renewal_remaining( $duration->max_renewals );
938 $circ->target_copy( $copy->id );
939 $circ->usr( $patron->id );
940 $circ->circ_lib( $ctx->{circ_lib} );
943 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
944 $circ->opac_renewal(1);
945 $circ->renewal_remaining($ctx->{renewal_remaining});
946 $circ->circ_staff($ctx->{requestor}->id);
950 # if the user provided an overiding checkout time,
951 # (e.g. the checkout really happened several hours ago), then
952 # we apply that here. Does this need a perm??
953 if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
954 $logger->debug("circ setting checkout_time to $ds");
955 $circ->xact_start($ds);
958 # if a patron is renewing, 'requestor' will be the patron
959 $circ->circ_staff($ctx->{requestor}->id );
960 _set_circ_due_date($circ);
961 $ctx->{circ} = $circ;
964 sub _apply_modified_due_date {
966 my $circ = $ctx->{circ};
970 if( $ctx->{due_date} ) {
972 my $evt = $U->check_perms(
973 $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
976 my $ds = _create_date_stamp($ctx->{due_date});
977 $logger->debug("circ modifying due_date to $ds");
978 $circ->due_date($ds);
982 # if the due_date lands on a day when the location is closed
983 my $copy = $ctx->{copy};
986 $logger->info("circ searching for closed date overlap on lib ".
987 $copy->circ_lib->id ." with an item due date of ".$circ->due_date );
989 my $dateinfo = $ctx->{session}->request(
990 'open-ils.storage.actor.org_unit.closed_date.overlap',
991 $copy->circ_lib->id, $circ->due_date )->gather(1);
995 $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
996 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
998 # XXX make the behavior more dynamic
999 # for now, we just push the due date to after the close date
1000 $circ->due_date($dateinfo->{end});
1007 sub _create_date_stamp {
1008 my $datestring = shift;
1009 return undef unless $datestring;
1010 $datestring = clense_ISO8601($datestring);
1011 $logger->debug("circ created date stamp => $datestring");
1015 sub _create_due_date {
1016 my $duration = shift;
1018 my ($sec,$min,$hour,$mday,$mon,$year) =
1019 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1020 $year += 1900; $mon += 1;
1021 my $due_date = sprintf(
1022 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1023 $year, $mon, $mday, $hour, $min, $sec);
1027 sub _set_circ_due_date {
1030 my $dd = _create_due_date($circ->duration);
1031 $logger->debug("Checkout setting due date on circ to: $dd");
1032 $circ->due_date($dd);
1035 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
1036 sub _update_checkout_copy {
1039 my $copy = $ctx->{copy};
1041 my $s = $U->copy_status_from_name('checked out');
1042 $copy->status( $s->id ) if $s;
1044 my $evt = $U->update_copy( session => $ctx->{session},
1045 copy => $copy, editor => $ctx->{requestor}->id );
1046 return (undef,$evt) if $evt;
1051 # commits the circ object to the db then fleshes the circ with rules objects
1052 sub _commit_checkout_circ_object {
1055 my $circ = $ctx->{circ};
1059 my $r = $ctx->{session}->request(
1060 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
1062 return $U->DB_UPDATE_FAILED($circ) unless $r;
1064 $logger->debug("Created a new circ object in checkout: $r");
1067 $circ->duration_rule($ctx->{duration_rule});
1068 $circ->max_fine_rule($ctx->{max_fine_rule});
1069 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
1075 # sees if there are any holds that this copy
1076 sub _handle_related_holds {
1079 my $copy = $ctx->{copy};
1080 my $patron = $ctx->{patron};
1081 my $holds = $holdcode->fetch_related_holds($copy->id);
1085 # XXX We should only fulfill one hold here...
1086 # XXX If a hold was transited to the user who is checking out
1087 # the item, we need to make sure that hold is what's grabbed
1088 if(ref($holds) && @$holds) {
1090 # for now, just sort by id to get what should be the oldest hold
1091 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1092 my @myholds = grep { $_->usr eq $patron->id } @$holds;
1093 my @altholds = grep { $_->usr ne $patron->id } @$holds;
1096 my $hold = $myholds[0];
1098 $logger->debug("Related hold found in checkout: " . $hold->id );
1100 $hold->current_copy($copy->id); # just make sure it's set
1101 # if the hold was never officially captured, capture it.
1102 $hold->capture_time('now') unless $hold->capture_time;
1103 $hold->fulfillment_time('now');
1104 my $r = $ctx->{session}->request(
1105 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
1106 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
1107 push( @fulfilled, $hold->id );
1110 # If there are any holds placed for other users that point to this copy,
1111 # then we need to un-target those holds so the targeter can pick a new copy
1114 $logger->info("Un-targeting hold ".$_->id.
1115 " because copy ".$copy->id." is getting checked out");
1117 $_->clear_current_copy;
1118 my $r = $ctx->{session}->request(
1119 "open-ils.storage.direct.action.hold_request.update", $_ )->gather(1);
1120 return (undef,$U->DB_UPDATE_FAILED( $_ )) unless $r;
1124 return (\@fulfilled, undef);
1127 sub _checkout_noncat {
1128 my ( $key, $requestor, $patron, %params ) = @_;
1129 my( $circ, $circlib, $evt );
1132 $circlib = $params{noncat_circ_lib} || $requestor->ws_ou;
1134 my $count = $params{noncat_count} || 1;
1135 my $cotime = _create_date_stamp($params{checkout_time}) || "";
1136 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1138 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1139 $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
1140 return $evt if $evt;
1143 return OpenILS::Event->new(
1144 'SUCCESS', payload => { noncat_circ => $circ } );
1148 __PACKAGE__->register_method(
1149 method => "generic_receive",
1150 api_name => "open-ils.circ.checkin",
1153 Generic super-method for handling all copies
1154 @param authtoken The login session key
1155 @param params Hash of named parameters including:
1156 barcode - The copy barcode
1157 force - If true, copies in bad statuses will be checked in and give good statuses
1162 __PACKAGE__->register_method(
1163 method => "generic_receive",
1164 api_name => "open-ils.circ.checkin.override",
1165 signature => q/@see open-ils.circ.checkin/
1168 sub generic_receive {
1169 my( $self, $conn, $authtoken, $params ) = @_;
1170 my( $ctx, $requestor, $evt );
1172 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1173 ( $requestor, $evt ) = $U->checksesperm(
1174 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1175 return $evt if $evt;
1178 my ($patron) = _find_patron_from_params($params);
1179 $ctx->{patron} = $patron if $patron;
1181 # load up the circ objects
1182 if( !( $ctx = $params->{_ctx}) ) {
1183 ( $ctx, $evt ) = create_circ_ctx( %$params,
1184 requestor => $requestor,
1185 session => $U->start_db_session(),
1187 fetch_copy_statuses => 1,
1188 fetch_copy_locations => 1,
1191 return $evt if $evt;
1193 $ctx->{override} = 1 if $self->api_name =~ /override/o;
1194 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1195 $ctx->{authtoken} = $authtoken;
1196 my $session = $ctx->{session};
1198 my $copy = $ctx->{copy};
1199 $U->unflesh_copy($copy);
1200 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $copy;
1202 $logger->info("Checkin copy called by user ".
1203 $requestor->id." for copy ".$copy->id);
1206 my $val = $self->checkin_do_receive($conn, $ctx);
1208 # ------------------------------------------------------------------------------
1209 # Update the patron penalty info in the DB
1210 # ------------------------------------------------------------------------------
1211 $U->update_patron_penalties(
1212 authtoken => $authtoken,
1213 patron => $ctx->{patron},
1220 sub checkin_do_receive {
1222 my( $self, $connection, $ctx ) = @_;
1225 my $copy = $ctx->{copy};
1226 my $session = $ctx->{session};
1227 my $requestor = $ctx->{requestor};
1228 my $change = 0; # did we actually do anything?
1233 # does the copy have an attached alert message?
1234 my $ae = _check_copy_alert($copy);
1235 push(@eventlist, $ae) if $ae;
1237 # is the copy is an a status we can't automatically resolve?
1238 $evt = _checkin_check_copy_status($ctx);
1239 push( @eventlist, $evt ) if $evt;
1242 # - see if the copy has an open circ attached
1243 #($ctx->{circ}, $evt) = $U->fetch_open_circulation($copy->id);
1244 ($ctx->{circ}, $evt) = $U->fetch_all_open_circulation($copy->id); # - get ones with stop fines as well
1245 return $evt if ($evt and $__isrenewal); # renewals require a circulation
1247 $circ = $ctx->{circ};
1249 # if the circ is marked as 'claims returned', add the event to the list
1250 push( @eventlist, OpenILS::Event->new('CIRC_CLAIMS_RETURNED') )
1251 if ($circ and $circ->stop_fines and $circ->stop_fines eq 'CLAIMSRETURNED');
1255 if($ctx->{override}) {
1256 $evt = override_events($requestor, $requestor->ws_ou, \@eventlist );
1257 return $evt if $evt;
1263 ($ctx->{transit}) = $U->fetch_open_transit_by_copy($copy->id);
1265 if( $ctx->{circ} ) {
1267 # There is an open circ on this item, close it out.
1269 $evt = _checkin_handle_circ($ctx);
1270 return $evt if $evt;
1272 } elsif( $ctx->{transit} ) {
1274 # is this item currently in transit?
1276 $evt = $transcode->transit_receive( $copy, $requestor, $session );
1277 my $holdtrans = $evt->{holdtransit};
1278 ($ctx->{hold}) = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1280 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1282 # either an error occurred or a ROUTE_ITEM was generated and the
1283 # item must be forwarded on to its destination.
1284 return _checkin_flesh_event($ctx, $evt);
1288 # Transit has been closed, now let's see if the copy's original
1289 # status is something the staff should be warned of
1290 my $e = _checkin_check_copy_status($ctx);
1295 # copy was received as a hold transit. Copy is at target lib
1296 # and hold transit is complete. We're done here...
1297 $U->commit_db_session($session);
1298 return _checkin_flesh_event($ctx, $evt);
1304 # ------------------------------------------------------------------------------
1305 # Circulations and transits are now closed where necessary. Now go on to see if
1306 # this copy can fulfill a hold or needs to be routed to a different location
1307 # ------------------------------------------------------------------------------
1310 # If it's a renewal, we're done
1312 $U->commit_db_session($session);
1313 return OpenILS::Event->new('SUCCESS');
1316 # Now, let's see if this copy is needed for a hold
1317 my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor );
1321 $ctx->{hold} = $hold;
1324 # Capture the hold with this copy
1325 return $evt if ($evt = _checkin_capture_hold($ctx));
1327 if( $hold->pickup_lib == $requestor->ws_ou ) {
1329 # This hold was captured in the correct location
1330 $evt = OpenILS::Event->new('SUCCESS');
1334 # Hold needs to be picked up elsewhere. Build a hold
1335 # transit and route the item.
1336 return $evt if ($evt =_checkin_build_hold_transit($ctx));
1337 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1340 } else { # not needed for a hold
1342 if( $copy->circ_lib == $requestor->ws_ou ) {
1344 # Copy is in the right place.
1345 $evt = OpenILS::Event->new('SUCCESS');
1347 # if the item happens to be a pre-cataloged item, send it
1348 # to cataloging and return the event
1349 my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1350 return $err if $err;
1356 # Copy wants to go home. Transit it there.
1357 return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1358 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1364 # ------------------------------------------------------------------
1365 # if the copy is not in a state that should persist,
1366 # set the copy to reshelving if it's not already there
1367 # ------------------------------------------------------------------
1368 my ($c, $e) = _reshelve_copy($ctx);
1370 $change = $c unless $change;
1374 $evt = OpenILS::Event->new('NO_CHANGE');
1375 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id)
1378 if( $copy->status == $U->copy_status_from_name('on holds shelf')->id );
1382 $U->commit_db_session($session);
1385 $logger->activity("checkin by user ".$requestor->id." on item ".
1386 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1388 return _checkin_flesh_event($ctx, $evt);
1391 sub _reshelve_copy {
1394 my $copy = $ctx->{copy};
1395 my $reqr = $ctx->{requestor};
1396 my $session = $ctx->{session};
1397 my $force = $ctx->{force};
1399 my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1402 $stat != $U->copy_status_from_name('on holds shelf')->id and
1403 $stat != $U->copy_status_from_name('available')->id and
1404 $stat != $U->copy_status_from_name('cataloging')->id and
1405 $stat != $U->copy_status_from_name('in transit')->id and
1406 $stat != $U->copy_status_from_name('reshelving')->id) ) {
1408 $copy->status( $U->copy_status_from_name('reshelving')->id );
1410 my $evt = $U->update_copy(
1412 editor => $reqr->id,
1413 session => $session,
1424 # returns undef if there are no 'open' claims-returned circs attached
1425 # to the given copy. if there is an open claims-returned circ,
1426 # then we check for override mode. if in override, mark the claims-returned
1427 # circ as checked in. if not, return event.
1428 sub _handle_claims_returned {
1430 my $copy = $ctx->{copy};
1432 my $CR = _fetch_open_claims_returned($copy->id);
1433 return undef unless $CR;
1435 # - If the caller has set the override flag, we will check the item in
1436 if($ctx->{override}) {
1438 $CR->checkin_time('now');
1439 $CR->checkin_lib($ctx->{requestor}->ws_ou);
1440 $CR->checkin_staff($ctx->{requestor}->id);
1442 my $stat = $U->storagereq(
1443 'open-ils.storage.direct.action.circulation.update', $CR);
1444 return $U->DB_UPDATE_FAILED($CR) unless $stat;
1445 return OpenILS::Event->new('SUCCESS');
1448 # - if not in override mode, return the CR event
1449 return OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1454 sub _fetch_open_claims_returned {
1456 my $trans = $U->storagereq(
1457 'open-ils.storage.direct.action.circulation.search_where',
1459 target_copy => $copyid,
1460 stop_fines => 'CLAIMSRETURNED',
1461 checkin_time => undef,
1464 return $$trans[0] if $trans && $$trans[0];
1469 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1470 sub _checkin_handle_precat {
1473 my $copy = $ctx->{copy};
1478 my $catstat = $U->copy_status_from_name('cataloging');
1480 if( $ctx->{precat} ) {
1482 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1484 if( $copy->status != $catstat->id ) {
1485 $copy->status($catstat->id);
1487 return (undef, 0, $errevt) if (
1488 $errevt = $U->update_copy(
1490 editor => $ctx->{requestor}->id,
1491 session => $ctx->{session} ));
1497 return ($evt, $change, undef);
1501 # returns the appropriate event for the given copy status
1502 # if the copy is not in a 'special' status, undef is returned
1503 sub _checkin_check_copy_status {
1505 my $copy = $ctx->{copy};
1506 my $reqr = $ctx->{requestor};
1507 my $ses = $ctx->{session};
1513 my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1516 if( $status == $U->copy_status_from_name('available')->id ||
1517 $status == $U->copy_status_from_name('checked out')->id ||
1518 $status == $U->copy_status_from_name('in process')->id ||
1519 $status == $U->copy_status_from_name('in transit')->id ||
1520 $status == $U->copy_status_from_name('reshelving')->id );
1522 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1523 if( $status == $U->copy_status_from_name('lost')->id );
1525 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1526 if( $status == $U->copy_status_from_name('missing')->id );
1528 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1535 # Just gets the copy back home. Returns undef on success, event on error
1536 sub _checkin_build_generic_copy_transit {
1539 my $requestor = $ctx->{requestor};
1540 my $copy = $ctx->{copy};
1541 my $transit = Fieldmapper::action::transit_copy->new;
1542 my $session = $ctx->{session};
1544 $logger->activity("User ". $requestor->id ." creating a ".
1545 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1547 $transit->source($requestor->ws_ou);
1548 $transit->dest($copy->circ_lib);
1549 $transit->target_copy($copy->id);
1550 $transit->source_send_time('now');
1551 $transit->copy_status($copy->status);
1553 $logger->debug("Creating new copy_transit in DB");
1555 my $s = $session->request(
1556 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1557 return $U->DB_UPDATE_FAILED($transit) unless $s;
1559 $logger->info("Checkin copy successfully created new transit: $s");
1561 $copy->status($U->copy_status_from_name('in transit')->id );
1563 return $U->update_copy( copy => $copy,
1564 editor => $requestor->id, session => $session );
1569 # returns event on error, undef on success
1570 sub _checkin_build_hold_transit {
1573 my $copy = $ctx->{copy};
1574 my $hold = $ctx->{hold};
1575 my $trans = Fieldmapper::action::hold_transit_copy->new;
1577 $trans->hold($hold->id);
1578 $trans->source($ctx->{requestor}->ws_ou);
1579 $trans->dest($hold->pickup_lib);
1580 $trans->source_send_time("now");
1581 $trans->target_copy($copy->id);
1582 $trans->copy_status($copy->status);
1584 my $id = $ctx->{session}->request(
1585 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1586 return $U->DB_UPDATE_FAILED($trans) unless $id;
1588 $logger->info("Checkin copy successfully created hold transit: $id");
1590 $copy->status($U->copy_status_from_name('in transit')->id );
1591 return $U->update_copy( copy => $copy,
1592 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1595 # Returns event on error, undef on success
1596 sub _checkin_capture_hold {
1598 my $copy = $ctx->{copy};
1599 my $hold = $ctx->{hold};
1601 $logger->debug("Checkin copy capturing hold ".$hold->id);
1603 $hold->current_copy($copy->id);
1604 $hold->capture_time('now');
1606 my $stat = $ctx->{session}->request(
1607 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1608 return $U->DB_UPDATE_FAILED($hold) unless $stat;
1610 $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1612 return $U->update_copy( copy => $copy,
1613 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1616 # fleshes an event with the relevant objects from the context
1617 sub _checkin_flesh_event {
1622 $payload->{copy} = $U->unflesh_copy($ctx->{copy});
1623 $payload->{record} = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1624 $payload->{circ} = $ctx->{circ} if $ctx->{circ};
1625 $payload->{transit} = $ctx->{transit} if $ctx->{transit};
1626 $payload->{hold} = $ctx->{hold} if $ctx->{hold};
1628 $evt->{payload} = $payload;
1633 # Closes out the circulation, puts the copy into reshelving.
1634 # Voids any bills attached to this circ after the backdate time
1635 # if a backdate is provided
1636 sub _checkin_handle_circ {
1640 my $circ = $ctx->{circ};
1641 my $copy = $ctx->{copy};
1642 my $requestor = $ctx->{requestor};
1643 my $session = $ctx->{session};
1647 $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1649 # backdate the circ if necessary
1650 if(my $backdate = $ctx->{backdate}) {
1651 return $evt if ($evt =
1652 _checkin_handle_backdate($backdate, $circ, $requestor, $session, 1));
1656 if(!$circ->stop_fines) {
1657 $circ->stop_fines('CHECKIN');
1658 $circ->stop_fines('RENEW') if $__isrenewal;
1659 $circ->stop_fines_time('now');
1662 # see if there are any fines owed on this circ. if not, close it
1663 ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1664 return $evt if $evt;
1665 $circ->xact_finish('now') if( $obt->balance_owed == 0 );
1667 # Set the checkin vars since we have the item
1668 $circ->checkin_time('now');
1669 $circ->checkin_staff($requestor->id);
1670 $circ->checkin_lib($requestor->ws_ou);
1672 $evt = _set_copy_reshelving($copy, $requestor->id, $ctx->{session});
1673 return $evt if $evt;
1675 $ctx->{session}->request(
1676 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1681 sub _set_copy_reshelving {
1682 my( $copy, $reqr, $session ) = @_;
1684 $logger->info("Setting copy ".$copy->id." to reshelving");
1685 $copy->status($U->copy_status_from_name('reshelving')->id);
1687 my $evt = $U->update_copy(
1688 session => $session,
1692 return $evt if $evt;
1695 # returns event on error, undef on success
1696 # This voids all bills attached to the given circulation that occurred
1697 # after the backdate
1698 # THIS DOES NOT CLOSE THE CIRC if there are no more fines on the item
1699 sub _checkin_handle_backdate {
1700 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1702 $logger->activity("User ".$requestor->id.
1703 " backdating circ [".$circ->target_copy."] to date: $backdate");
1705 my $bills = $session->request( # XXX Verify this call is correct
1706 "open-ils.storage.direct.money.billing.search_where.atomic",
1707 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1710 for my $bill (@$bills) {
1712 my $n = $bill->note || "";
1713 $bill->note($n . "\nSYSTEM VOIDED FOR BACKDATE");
1714 my $s = $session->request(
1715 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1716 return $U->DB_UPDATE_FAILED($bill) unless $s;
1720 # if the caller elects to attempt to close the circulation
1721 # transaction, then it will be closed if there are not further
1722 # charges on the transaction
1724 #my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1725 #return $evt if $evt;
1726 #$circ->xact_finish($backdate) if $obt->balance_owed <= 0;
1733 sub _find_patron_from_params {
1741 if(my $barcode = $params->{barcode}) {
1742 $logger->debug("circ finding user from params with barcode $barcode");
1743 ($copy, $evt) = $U->fetch_copy_by_barcode($barcode);
1744 return (undef, undef, $evt) if $evt;
1745 ($circ, $evt) = $U->fetch_open_circulation($copy->id);
1746 return (undef, undef, $evt) if $evt;
1747 ($patron, $evt) = $U->fetch_user($circ->usr);
1748 return (undef, undef, $evt) if $evt;
1750 return ($patron, $copy);
1754 # ------------------------------------------------------------------------------
1756 __PACKAGE__->register_method(
1758 api_name => "open-ils.circ.renew.override",
1759 signature => q/@see open-ils.circ.renew/,
1763 __PACKAGE__->register_method(
1765 api_name => "open-ils.circ.renew",
1766 notes => <<" NOTES");
1767 PARAMS( authtoken, circ => circ_id );
1768 open-ils.circ.renew(login_session, circ_object);
1769 Renews the provided circulation. login_session is the requestor of the
1770 renewal and if the logged in user is not the same as circ->usr, then
1771 the logged in user must have RENEW_CIRC permissions.
1775 my( $self, $client, $authtoken, $params ) = @_;
1778 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1781 $params->{override} = 1 if $self->api_name =~ /override/o;
1783 # fetch the patron object one way or another
1784 if( $params->{patron} ) {
1785 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1786 if($evt) { $__isrenewal = 0; return $evt; }
1788 } elsif( $params->{patron_barcode} ) {
1789 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1790 if($evt) { $__isrenewal = 0; return $evt; }
1793 ($patron, $copy, $evt) = _find_patron_from_params($params);
1794 if($evt) { $__isrenewal = 0; return $evt; }
1795 $params->{copy} = $copy;
1798 # verify our login session
1799 ($requestor, $evt) = $U->checkses($authtoken);
1800 if($evt) { $__isrenewal = 0; return $evt; }
1802 # make sure we have permission to perform a renewal
1803 if( $requestor->id ne $patron->id ) {
1804 $evt = $U->check_perms($requestor->id, $requestor->ws_ou, 'RENEW_CIRC');
1805 if($evt) { $__isrenewal = 0; return $evt; }
1809 # fetch and build the circulation environment
1810 ( $ctx, $evt ) = create_circ_ctx( %$params,
1812 requestor => $requestor,
1815 fetch_copy_statuses => 1,
1816 fetch_copy_locations => 1,
1818 if($evt) { $__isrenewal = 0; return $evt; }
1819 $params->{_ctx} = $ctx;
1821 # make sure they have some renewals left and make sure the circulation exists
1822 ($circ, $evt) = _check_renewal_remaining($ctx);
1823 if($evt) { $__isrenewal = 0; return $evt; }
1824 $ctx->{old_circ} = $circ;
1825 my $renewals = $circ->renewal_remaining - 1;
1827 # run the renew permit script
1828 $evt = _run_renew_scripts($ctx);
1829 if($evt) { $__isrenewal = 0; return $evt; }
1832 #$ctx->{patron} = $ctx->{patron}->id;
1833 $evt = $self->generic_receive($client, $authtoken, $ctx );
1834 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1836 if( !$U->event_equals($evt, 'SUCCESS') ) {
1837 $__isrenewal = 0; return $evt;
1840 # re-fetch the context since objects have changed in the checkin
1841 # XXX Do we really need to do this - what changes that we don't control??
1842 ( $ctx, $evt ) = create_circ_ctx( %$params,
1844 requestor => $requestor,
1847 fetch_copy_statuses => 1,
1848 fetch_copy_locations => 1,
1850 if($evt) { $__isrenewal = 0; return $evt; }
1851 $params->{_ctx} = $ctx;
1852 $ctx->{renewal_remaining} = $renewals;
1854 # run the circ permit scripts
1855 if( $ctx->{permit_override} ) {
1856 $evt = $U->check_perms(
1857 $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1858 if($evt) { $__isrenewal = 0; return $evt; }
1861 $evt = $self->permit_circ( $client, $authtoken, $params );
1862 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1863 $params->{precat} = 1;
1866 if(!$U->event_equals($evt, 'SUCCESS')) {
1867 if($evt) { $__isrenewal = 0; return $evt; }
1870 $params->{permit_key} = $evt->{payload};
1874 # checkout the item again
1875 $params->{patron} = $ctx->{patron}->id;
1876 $evt = $self->checkout($client, $authtoken, $params );
1878 $logger->activity("user ".$requestor->id." renewl of item ".
1879 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1885 sub _check_renewal_remaining {
1888 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1889 return (undef, $evt) if $evt;
1890 $evt = OpenILS::Event->new(
1891 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1892 return ($circ, $evt);
1895 sub _run_renew_scripts {
1897 my $runner = $ctx->{runner};
1900 $runner->load($scripts{circ_permit_renew});
1901 my $result = $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1902 my $events = $result->{events};
1904 $logger->activity("circ_permit_renew for user ".
1905 $ctx->{patron}->id." returned events: @$events") if @$events;
1908 push( @allevents, OpenILS::Event->new($_)) for @$events;
1909 return \@allevents if @allevents;