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 $logger->debug("script builder events: : @evts") if @evts;
107 if(!$params{noncat}) {
108 if( @evts and grep { $_->{textcode} eq 'ASSET_COPY_NOT_FOUND' } @evts) {
111 $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
115 warn "PRECAT = TRUE\n" if $ctx->{precat};
117 _build_circ_script_runner($ctx);
122 # $evt = _ctx_add_patron_objects($ctx, %params);
123 # return (undef,$evt) if $evt;
125 # if(!$params{noncat}) {
126 # if( $evt = _ctx_add_copy_objects($ctx, %params) ) {
127 # $ctx->{precat} = 1 if($evt->{textcode} eq 'ASSET_COPY_NOT_FOUND')
129 # $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
133 # _doctor_patron_object($ctx) if $ctx->{patron};
134 # _doctor_copy_object($ctx) if $ctx->{copy};
136 # if(!$ctx->{no_runner}) {
137 # _build_circ_script_runner($ctx);
138 # _add_script_runner_methods($ctx);
145 #sub _ctx_add_patron_objects {
146 # my( $ctx, %params) = @_;
149 # $cache{group_tree} = $U->fetch_permission_group_tree() unless $cache{group_tree};
150 # $ctx->{group_tree} = $cache{group_tree};
152 # $ctx->{patron_circ_summary} =
153 # $U->fetch_patron_circ_summary($ctx->{patron}->id)
154 # if $params{fetch_patron_circsummary};
160 sub _find_copy_by_attr {
165 my $copy = $params{copy} || undef;
170 $U->fetch_copy($params{copyid}) if $params{copyid};
171 return (undef,$evt) if $evt;
175 $U->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
176 return (undef,$evt) if $evt;
179 return ( $copy, $evt );
183 #sub _ctx_add_copy_objects {
184 # my($ctx, %params) = @_;
189 # $cache{copy_statuses} = $U->fetch_copy_statuses
190 # if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
192 # $cache{copy_locations} = $U->fetch_copy_locations
193 # if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
195 # $ctx->{copy_statuses} = $cache{copy_statuses};
196 # $ctx->{copy_locations} = $cache{copy_locations};
198 # ($copy, $evt) = _find_copy_by_attr(%params);
199 # return $evt if $evt;
201 # if( $copy and !$ctx->{title} ) {
203 # my $r = $RECORD_FROM_COPY_CACHE{$copy->id};
204 # ($r, $evt) = $U->fetch_record_by_copy( $copy->id ) unless $r;
205 # return $evt if $evt;
206 # $RECORD_FROM_COPY_CACHE{$copy->id} = $r;
208 # $ctx->{title} = $r;
209 # $ctx->{copy} = $copy;
211 # ($ctx->{volume}) = $U->fetch_callnumber($copy->call_number);
212 # $ctx->{recordDescriptor} = $U->cstorereq(
213 # 'open-ils.cstore.direct.metabib.record_descriptor.search',
214 # { record => $ctx->{title}->id });
223 ## ------------------------------------------------------------------------------
224 ## Fleshes parts of the patron object
225 ## ------------------------------------------------------------------------------
226 #sub _doctor_copy_object {
229 # my $copy = $ctx->{copy} || return undef;
231 # $logger->debug("Doctoring copy object...");
233 # # set the copy status to a status name
234 # $copy->status( _get_copy_status( $copy, $ctx->{copy_statuses} ) );
236 # # set the copy location to the location object
237 # $copy->location( _get_copy_location( $copy, $ctx->{copy_locations} ) );
239 # $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
244 ## ------------------------------------------------------------------------------
245 ## Fleshes parts of the patron object
246 ## ------------------------------------------------------------------------------
247 #sub _doctor_patron_object {
250 # my $patron = $ctx->{patron} || return undef;
252 # # set the patron ptofile to the profile name
253 # $patron->profile( _get_patron_profile(
254 # $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
256 # # flesh the org unit
258 # $U->fetch_org_unit( $patron->home_ou ) ) if $patron;
262 ## recurse and find the patron profile name from the tree
263 ## another option would be to grab the groups for the patron
264 ## and cycle through those until the "profile" group has been found
265 #sub _get_patron_profile {
266 # my( $patron, $group_tree ) = @_;
267 # return $group_tree if ($group_tree->id eq $patron->profile);
268 # return undef unless ($group_tree->children);
270 # for my $child (@{$group_tree->children}) {
271 # my $ret = _get_patron_profile( $patron, $child );
272 # return $ret if $ret;
277 #sub _get_copy_status {
278 # my( $copy, $cstatus ) = @_;
281 # for my $status (@$cstatus) {
282 # $s = $status if( $status->id eq $copy->status )
284 # $logger->debug("Retrieving copy status: " . $s->name) if $s;
288 #sub _get_copy_location {
289 # my( $copy, $locations ) = @_;
292 # for my $loc (@$locations) {
293 # $l = $loc if $loc->id eq $copy->location;
295 # $logger->debug("Retrieving copy location: " . $l->name ) if $l;
300 # ------------------------------------------------------------------------------
301 # Constructs and shoves data into the script environment
302 # ------------------------------------------------------------------------------
303 sub _build_circ_script_runner {
307 $logger->debug("Loading script environment for circulation");
310 my $runner = $ctx->{runner};
313 $runner->insert('environment.isRenewal', 1);
315 $runner->insert('environment.isRenewal', undef);
318 if($ctx->{ishold} ) {
319 $runner->insert('environment.isHold', 1);
321 $runner->insert('environment.isHold', undef)
324 if( $ctx->{noncat} ) {
325 $runner->insert('environment.isNonCat', 1);
326 $runner->insert('environment.nonCatType', $ctx->{noncat_type});
328 $runner->insert('environment.isNonCat', undef);
332 $logger->debug("Loading circ script lib path $_");
333 $runner->add_path( $_ );
345 # for(@$script_libs) {
346 # $logger->debug("Loading circ script lib path $_");
347 # $runner->add_path( $_ );
350 # # Note: inserting the number 0 into the script turns into the
351 # # string "0", and thus evaluates to true in JS land
352 # # inserting undef will insert "", which evaluates to false
354 # $runner->insert( 'environment.patron', $ctx->{patron}, 1);
355 # $runner->insert( 'environment.record', $ctx->{title}, 1);
356 # $runner->insert( 'environment.copy', $ctx->{copy}, 1);
357 # $runner->insert( 'environment.volume', $ctx->{volume}, 1);
358 # $runner->insert( 'environment.recordDescriptor', $ctx->{recordDescriptor}, 1);
359 # $runner->insert( 'environment.requestor', $ctx->{requestor}, 1);
361 # # circ script result
362 # $runner->insert( 'result', {} );
363 # #$runner->insert( 'result.event', 'SUCCESS' );
364 # $runner->insert( 'result.events', [] );
367 # $runner->insert('environment.isRenewal', 1);
369 # $runner->insert('environment.isRenewal', undef);
372 # if($ctx->{ishold} ) {
373 # $runner->insert('environment.isHold', 1);
375 # $runner->insert('environment.isHold', undef)
378 # if( $ctx->{noncat} ) {
379 # $runner->insert('environment.isNonCat', 1);
380 # $runner->insert('environment.nonCatType', $ctx->{noncat_type});
382 # $runner->insert('environment.isNonCat', undef);
385 # if(ref($ctx->{patron_circ_summary})) {
386 # $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
389 # $ctx->{runner} = $runner;
400 #sub _add_script_runner_methods {
403 # my $runner = $ctx->{runner};
405 # if( $ctx->{copy} ) {
407 # # allows a script to fetch a hold that is currently targeting the
409 # $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
411 # my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
412 # $hold = undef unless $hold;
413 # $runner->insert( $key, $hold, 1 );
419 # ------------------------------------------------------------------------------
422 __PACKAGE__->register_method(
423 method => "permit_circ",
424 api_name => "open-ils.circ.checkout.permit",
426 Determines if the given checkout can occur
427 @param authtoken The login session key
428 @param params A trailing hash of named params including
429 barcode : The copy barcode,
430 patron : The patron the checkout is occurring for,
431 renew : true or false - whether or not this is a renewal
432 @return The event that occurred during the permit check.
435 __PACKAGE__->register_method (
436 method => 'permit_circ',
437 api_name => 'open-ils.circ.checkout.permit.override',
438 signature => q/@see open-ils.circ.checkout.permit/,
442 my( $self, $client, $authtoken, $params ) = @_;
445 my $override = $params->{override} = 1 if $self->api_name =~ /override/o;
447 my ( $requestor, $patron, $ctx, $evt, $circ );
449 # check permisson of the requestor
450 ( $requestor, $patron, $evt ) =
451 $U->checkses_requestor(
452 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
456 # fetch and build the circulation environment
457 if( !( $ctx = $params->{_ctx}) ) {
459 ( $ctx, $evt ) = create_circ_ctx( %$params,
461 requestor => $requestor,
463 #fetch_patron_circ_summary => 1,
464 fetch_copy_statuses => 1,
465 fetch_copy_locations => 1,
471 my $copy = $ctx->{copy};
473 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
474 return OpenILS::Event->new('COPY_IN_TRANSIT')
475 if $stat == $U->copy_status_from_name('in transit')->id;
478 $ctx->{authtoken} = $authtoken;
481 if( $ctx->{copy} and ($evt = _handle_claims_returned($ctx)) ) {
482 return $evt unless $U->event_equals($evt, 'SUCCESS');
490 # no claims returned circ was found, check if there is any open circ
491 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
492 ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
493 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
498 $ctx->{permit_key} = _cache_permit_key();
499 my $events = _run_permit_scripts($ctx);
502 $evt = override_events($requestor, $requestor->ws_ou,
503 $events, $authtoken, $client);
505 return OpenILS::Event->new(
506 'ITEM_NOT_CATALOGED', payload => $ctx->{permit_key}) if $ctx->{precat};
507 return OpenILS::Event->new('SUCCESS', payload => $ctx->{permit_key} );
513 sub override_events {
515 my( $requestor, $org, $events, $authtoken, $conn ) = @_;
516 $events = [ $events ] unless ref($events) eq 'ARRAY';
519 for my $e (@$events) {
520 my $tc = $e->{textcode};
521 next if $tc eq 'SUCCESS';
522 my $ov = "$tc.override";
523 $logger->info("attempting to override event $ov");
524 my $evt = $U->check_perms( $requestor->id, $org, $ov );
534 # Runs the patron and copy permit scripts
535 # if this is a non-cat circulation, the copy permit script
537 sub _run_permit_scripts {
540 my $runner = $ctx->{runner};
541 my $patronid = $ctx->{patron}->id;
542 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
543 my $key = $ctx->{permit_key};
546 # ---------------------------------------------------------------------
547 # Find all of the fatal penalties currently set on the user
548 # ---------------------------------------------------------------------
549 my $penalties = $U->update_patron_penalties(
550 authtoken => $ctx->{authtoken},
551 patron => $ctx->{patron}
554 $penalties = $penalties->{fatal_penalties};
555 $logger->info("circ patron penalties user $patronid: @$penalties");
558 # ---------------------------------------------------------------------
559 # Now run the patron permit script
560 # ---------------------------------------------------------------------
561 $logger->debug("Running circ script: " . $scripts{circ_permit_patron});
563 $runner->load($scripts{circ_permit_patron});
564 my $result = $runner->run or
565 throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
567 my $patron_events = $result->{events};
568 $ctx->{circ_permit_patron_events} = $patron_events;
569 $logger->activity("circ_permit_patron for returned @$patron_events") if @$patron_events;
571 my @evts_so_far = (@$penalties, @$patron_events);
573 push( @allevents, OpenILS::Event->new($_)) for @evts_so_far;
576 return \@allevents if @allevents;
579 warn "Item is precat in checkout permit\n";
580 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
581 #push( @allevents, OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key));
582 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
585 if( $ctx->{noncat} ) {
586 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
587 return OpenILS::Event->new('SUCCESS', payload => $key);
592 $logger->debug("Exiting circ permit early because request is for hold patron permit");
593 return OpenILS::Event->new('SUCCESS');
598 # ---------------------------------------------------------------------
599 # Capture all of the copy permit events
600 # ---------------------------------------------------------------------
601 $runner->load($scripts{circ_permit_copy});
602 $result = $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
603 my $copy_events = $result->{events};
605 $ctx->{circ_permit_copy_events} = $copy_events;
606 $logger->activity("circ_permit_copy for copy ".
607 "$barcode returned events: @$copy_events") if @$copy_events;
612 # ---------------------------------------------------------------------
613 # Now collect all of the events together
614 # ---------------------------------------------------------------------
616 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
618 my $ae = _check_copy_alert($ctx->{copy});
619 push( @allevents, $ae ) if $ae;
621 return OpenILS::Event->new('SUCCESS', payload => $key) unless (@allevents);
624 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
625 @allevents = values %hash;
628 $_->{payload} = $ctx->{copy}->status->id
629 if ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
635 sub _check_copy_alert {
637 return OpenILS::Event->new('COPY_ALERT_MESSAGE',
638 payload => $copy->alert_message) if $copy->alert_message;
642 # takes copyid, patronid, and requestor id
643 sub _cache_permit_key {
644 my $key = md5_hex( time() . rand() . "$$" );
645 $logger->debug("Setting circ permit key to $key");
646 $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
650 sub _check_permit_key {
652 $logger->debug("Fetching circ permit key $key");
653 my $k = "oils_permit_key_$key";
654 my $one = $cache_handle->get_cache($k);
655 $cache_handle->delete_cache($k);
656 return ($one) ? 1 : 0;
660 # ------------------------------------------------------------------------------
662 __PACKAGE__->register_method(
663 method => "checkout",
664 api_name => "open-ils.circ.checkout",
667 @param authtoken The login session key
668 @param params A named hash of params including:
670 barcode If no copy is provided, the copy is retrieved via barcode
671 copyid If no copy or barcode is provide, the copy id will be use
672 patron The patron's id
673 noncat True if this is a circulation for a non-cataloted item
674 noncat_type The non-cataloged type id
675 noncat_circ_lib The location for the noncat circ.
676 precat The item has yet to be cataloged
677 dummy_title The temporary title of the pre-cataloded item
678 dummy_author The temporary authr of the pre-cataloded item
679 Default is the home org of the staff member
680 @return The SUCCESS event on success, any other event depending on the error
684 my( $self, $client, $authtoken, $params ) = @_;
687 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
688 my $key = $params->{permit_key};
690 # if this is a renewal, then the requestor does not have to
691 # have checkout privelages
692 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
693 ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
696 if( $params->{patron} ) {
697 ( $patron, $evt ) = $U->fetch_user($params->{patron});
700 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
704 # set the circ lib to the home org of the requestor if not specified
705 my $circlib = (defined($params->{circ_lib})) ?
706 $params->{circ_lib} : $requestor->ws_ou;
709 # Make sure the caller has a valid permit key or is
710 # overriding the permit can
711 if( $params->{permit_override} ) {
712 $evt = $U->check_perms(
713 $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
717 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
718 unless _check_permit_key($key);
721 # if this is a non-cataloged item, check it out and return
722 return _checkout_noncat(
723 $key, $requestor, $patron, %$params ) if $params->{noncat};
725 # if this item has yet to be cataloged, make sure a dummy copy exists
726 ( $params->{copy}, $evt ) = _make_precat_copy(
727 $requestor, $circlib, $params ) if $params->{precat};
731 # fetch and build the circulation environment
732 if( !( $ctx = $params->{_ctx}) ) {
733 ( $ctx, $evt ) = create_circ_ctx( %$params,
735 requestor => $requestor,
736 session => $U->start_db_session(),
738 fetch_copy_statuses => 1,
739 fetch_copy_locations => 1,
743 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
745 # if the call doesn't know it's not cataloged..
746 if(!$params->{precat}) {
747 if( $ctx->{copy}->call_number eq '-1' ) {
748 return OpenILS::Event->new('ITEM_NOT_CATALOGED');
753 $copy = $ctx->{copy};
755 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
756 return OpenILS::Event->new('COPY_IN_TRANSIT')
757 if $stat == $U->copy_status_from_name('in transit')->id;
760 # this happens in permit.. but we need to check here for 'offline' requests
761 ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
762 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
764 my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
767 $ctx->{circ_lib} = $circlib;
769 $evt = _run_checkout_scripts($ctx);
773 _build_checkout_circ_object($ctx);
775 $evt = _apply_modified_due_date($ctx);
778 $evt = _commit_checkout_circ_object($ctx);
781 $evt = _update_checkout_copy($ctx);
785 ($holds, $evt) = _handle_related_holds($ctx);
789 $logger->debug("Checkout committing objects with session thread trace: ".$ctx->{session}->session_id);
790 $U->commit_db_session($ctx->{session});
791 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
793 $logger->activity("user ".$requestor->id." successfully checked out item ".
794 $ctx->{copy}->barcode." to user ".$ctx->{patron}->id );
797 # ------------------------------------------------------------------------------
798 # Update the patron penalty info in the DB
799 # ------------------------------------------------------------------------------
800 $U->update_patron_penalties(
801 authtoken => $authtoken,
802 patron => $ctx->{patron} ,
806 return OpenILS::Event->new('SUCCESS',
808 copy => $U->unflesh_copy($ctx->{copy}),
809 circ => $ctx->{circ},
811 holds_fulfilled => $holds,
817 sub _make_precat_copy {
818 my ( $requestor, $circlib, $params ) = @_;
820 my( $copy, undef ) = _find_copy_by_attr(%$params);
823 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
825 $copy->editor($requestor->id);
826 $copy->edit_date('now');
827 $copy->dummy_title($params->{dummy_title});
828 $copy->dummy_author($params->{dummy_author});
830 my $stat = $U->storagereq(
831 'open-ils.storage.direct.asset.copy.update', $copy );
833 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $stat;
837 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
839 my $evt = OpenILS::Event->new(
840 'BAD_PARAMS', desc => "Dummy title or author not provided" )
841 unless ( $params->{dummy_title} and $params->{dummy_author} );
842 return (undef, $evt) if $evt;
844 $copy = Fieldmapper::asset::copy->new;
845 $copy->circ_lib($circlib);
846 $copy->creator($requestor->id);
847 $copy->editor($requestor->id);
848 $copy->barcode($params->{barcode});
849 $copy->call_number(-1); #special CN for precat materials
850 $copy->loan_duration(&PRECAT_LOAN_DURATION);
851 $copy->fine_level(&PRECAT_FINE_LEVEL);
853 $copy->dummy_title($params->{dummy_title});
854 $copy->dummy_author($params->{dummy_author});
856 my $id = $U->storagereq(
857 'open-ils.storage.direct.asset.copy.create', $copy );
858 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
860 $logger->debug("Pre-cataloged copy successfully created");
861 return ($U->fetch_copy($id));
865 sub _run_checkout_scripts {
871 my $runner = $ctx->{runner};
873 # $runner->insert('result.durationLevel');
874 # $runner->insert('result.durationRule');
875 # $runner->insert('result.recurringFinesRule');
876 # $runner->insert('result.recurringFinesLevel');
877 # $runner->insert('result.maxFine');
879 $runner->load($scripts{circ_duration});
881 my $result = $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
882 my $duration = $result->{durationRule};
883 my $dur_level = $result->{durationLevel};
884 my $recurring = $result->{recurringFinesRule};
885 my $rec_fines_level = $result->{recurringFinesLevel};
886 my $max_fine = $result->{maxFine};
888 # $runner->load($scripts{circ_recurring_fines});
889 # $result = $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
890 # my $recurring = $result->{recurringFinesRule};
891 # my $rec_fines_level = $result->{recurringFinesLevel};
892 # $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
894 # $runner->load($scripts{circ_max_fines});
895 # $result = $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
896 # my $max_fine = $result->{maxFine};
897 # $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
899 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
901 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
903 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
906 $ctx->{duration_level} = $dur_level;
907 $ctx->{recurring_fines_level} = $rec_fines_level;
908 $ctx->{duration_rule} = $duration;
909 $ctx->{recurring_fines_rule} = $recurring;
910 $ctx->{max_fine_rule} = $max_fine;
915 sub _build_checkout_circ_object {
919 my $circ = new Fieldmapper::action::circulation;
920 my $duration = $ctx->{duration_rule};
921 my $max = $ctx->{max_fine_rule};
922 my $recurring = $ctx->{recurring_fines_rule};
923 my $copy = $ctx->{copy};
924 my $patron = $ctx->{patron};
925 my $dur_level = $ctx->{duration_level};
926 my $rec_level = $ctx->{recurring_fines_level};
928 $circ->duration( $duration->shrt ) if ($dur_level == 1);
929 $circ->duration( $duration->normal ) if ($dur_level == 2);
930 $circ->duration( $duration->extended ) if ($dur_level == 3);
932 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
933 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
934 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
936 $circ->duration_rule( $duration->name );
937 $circ->recuring_fine_rule( $recurring->name );
938 $circ->max_fine_rule( $max->name );
939 $circ->max_fine( $max->amount );
941 $circ->fine_interval($recurring->recurance_interval);
942 $circ->renewal_remaining( $duration->max_renewals );
943 $circ->target_copy( $copy->id );
944 $circ->usr( $patron->id );
945 $circ->circ_lib( $ctx->{circ_lib} );
948 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
949 $circ->opac_renewal(1);
950 $circ->renewal_remaining($ctx->{renewal_remaining});
951 $circ->circ_staff($ctx->{requestor}->id);
955 # if the user provided an overiding checkout time,
956 # (e.g. the checkout really happened several hours ago), then
957 # we apply that here. Does this need a perm??
958 if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
959 $logger->debug("circ setting checkout_time to $ds");
960 $circ->xact_start($ds);
963 # if a patron is renewing, 'requestor' will be the patron
964 $circ->circ_staff($ctx->{requestor}->id );
965 _set_circ_due_date($circ);
966 $ctx->{circ} = $circ;
969 sub _apply_modified_due_date {
971 my $circ = $ctx->{circ};
975 if( $ctx->{due_date} ) {
977 my $evt = $U->check_perms(
978 $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
981 my $ds = _create_date_stamp($ctx->{due_date});
982 $logger->debug("circ modifying due_date to $ds");
983 $circ->due_date($ds);
987 # if the due_date lands on a day when the location is closed
988 my $copy = $ctx->{copy};
991 $logger->info("circ searching for closed date overlap on lib ".
992 $copy->circ_lib->id ." with an item due date of ".$circ->due_date );
994 my $dateinfo = $ctx->{session}->request(
995 'open-ils.storage.actor.org_unit.closed_date.overlap',
996 $copy->circ_lib->id, $circ->due_date )->gather(1);
1000 $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
1001 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1003 # XXX make the behavior more dynamic
1004 # for now, we just push the due date to after the close date
1005 $circ->due_date($dateinfo->{end});
1012 sub _create_date_stamp {
1013 my $datestring = shift;
1014 return undef unless $datestring;
1015 $datestring = clense_ISO8601($datestring);
1016 $logger->debug("circ created date stamp => $datestring");
1020 sub _create_due_date {
1021 my $duration = shift;
1023 my ($sec,$min,$hour,$mday,$mon,$year) =
1024 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1025 $year += 1900; $mon += 1;
1026 my $due_date = sprintf(
1027 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1028 $year, $mon, $mday, $hour, $min, $sec);
1032 sub _set_circ_due_date {
1035 my $dd = _create_due_date($circ->duration);
1036 $logger->debug("Checkout setting due date on circ to: $dd");
1037 $circ->due_date($dd);
1040 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
1041 sub _update_checkout_copy {
1044 my $copy = $ctx->{copy};
1046 my $s = $U->copy_status_from_name('checked out');
1047 $copy->status( $s->id ) if $s;
1049 my $evt = $U->update_copy( session => $ctx->{session},
1050 copy => $copy, editor => $ctx->{requestor}->id );
1051 return (undef,$evt) if $evt;
1056 # commits the circ object to the db then fleshes the circ with rules objects
1057 sub _commit_checkout_circ_object {
1060 my $circ = $ctx->{circ};
1064 my $r = $ctx->{session}->request(
1065 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
1067 return $U->DB_UPDATE_FAILED($circ) unless $r;
1069 $logger->debug("Created a new circ object in checkout: $r");
1072 $circ->duration_rule($ctx->{duration_rule});
1073 $circ->max_fine_rule($ctx->{max_fine_rule});
1074 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
1080 # sees if there are any holds that this copy
1081 sub _handle_related_holds {
1084 my $copy = $ctx->{copy};
1085 my $patron = $ctx->{patron};
1086 my $holds = $holdcode->fetch_related_holds($copy->id);
1090 # XXX We should only fulfill one hold here...
1091 # XXX If a hold was transited to the user who is checking out
1092 # the item, we need to make sure that hold is what's grabbed
1093 if(ref($holds) && @$holds) {
1095 # for now, just sort by id to get what should be the oldest hold
1096 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1097 my @myholds = grep { $_->usr eq $patron->id } @$holds;
1098 my @altholds = grep { $_->usr ne $patron->id } @$holds;
1101 my $hold = $myholds[0];
1103 $logger->debug("Related hold found in checkout: " . $hold->id );
1105 $hold->current_copy($copy->id); # just make sure it's set
1106 # if the hold was never officially captured, capture it.
1107 $hold->capture_time('now') unless $hold->capture_time;
1108 $hold->fulfillment_time('now');
1109 my $r = $ctx->{session}->request(
1110 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
1111 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
1112 push( @fulfilled, $hold->id );
1115 # If there are any holds placed for other users that point to this copy,
1116 # then we need to un-target those holds so the targeter can pick a new copy
1119 $logger->info("Un-targeting hold ".$_->id.
1120 " because copy ".$copy->id." is getting checked out");
1122 $_->clear_current_copy;
1123 my $r = $ctx->{session}->request(
1124 "open-ils.storage.direct.action.hold_request.update", $_ )->gather(1);
1125 return (undef,$U->DB_UPDATE_FAILED( $_ )) unless $r;
1129 return (\@fulfilled, undef);
1132 sub _checkout_noncat {
1133 my ( $key, $requestor, $patron, %params ) = @_;
1134 my( $circ, $circlib, $evt );
1137 $circlib = $params{noncat_circ_lib} || $requestor->ws_ou;
1139 my $count = $params{noncat_count} || 1;
1140 my $cotime = _create_date_stamp($params{checkout_time}) || "";
1141 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1143 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1144 $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
1145 return $evt if $evt;
1148 return OpenILS::Event->new(
1149 'SUCCESS', payload => { noncat_circ => $circ } );
1153 __PACKAGE__->register_method(
1154 method => "generic_receive",
1155 api_name => "open-ils.circ.checkin",
1158 Generic super-method for handling all copies
1159 @param authtoken The login session key
1160 @param params Hash of named parameters including:
1161 barcode - The copy barcode
1162 force - If true, copies in bad statuses will be checked in and give good statuses
1167 __PACKAGE__->register_method(
1168 method => "generic_receive",
1169 api_name => "open-ils.circ.checkin.override",
1170 signature => q/@see open-ils.circ.checkin/
1173 sub generic_receive {
1174 my( $self, $conn, $authtoken, $params ) = @_;
1175 my( $ctx, $requestor, $evt );
1177 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1178 ( $requestor, $evt ) = $U->checksesperm(
1179 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1180 return $evt if $evt;
1183 my ($patron) = _find_patron_from_params($params);
1184 $ctx->{patron} = $patron if $patron;
1186 # load up the circ objects
1187 if( !( $ctx = $params->{_ctx}) ) {
1188 ( $ctx, $evt ) = create_circ_ctx( %$params,
1189 requestor => $requestor,
1190 session => $U->start_db_session(),
1192 fetch_copy_statuses => 1,
1193 fetch_copy_locations => 1,
1196 return $evt if $evt;
1198 $ctx->{override} = 1 if $self->api_name =~ /override/o;
1199 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1200 $ctx->{authtoken} = $authtoken;
1201 my $session = $ctx->{session};
1203 my $copy = $ctx->{copy};
1204 $U->unflesh_copy($copy);
1205 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $copy;
1207 $logger->info("Checkin copy called by user ".
1208 $requestor->id." for copy ".$copy->id);
1211 my $val = $self->checkin_do_receive($conn, $ctx);
1213 # ------------------------------------------------------------------------------
1214 # Update the patron penalty info in the DB
1215 # ------------------------------------------------------------------------------
1216 $U->update_patron_penalties(
1217 authtoken => $authtoken,
1218 patron => $ctx->{patron},
1225 sub checkin_do_receive {
1227 my( $self, $connection, $ctx ) = @_;
1230 my $copy = $ctx->{copy};
1231 my $session = $ctx->{session};
1232 my $requestor = $ctx->{requestor};
1233 my $change = 0; # did we actually do anything?
1238 # does the copy have an attached alert message?
1239 my $ae = _check_copy_alert($copy);
1240 push(@eventlist, $ae) if $ae;
1242 # is the copy is an a status we can't automatically resolve?
1243 $evt = _checkin_check_copy_status($ctx);
1244 push( @eventlist, $evt ) if $evt;
1247 # - see if the copy has an open circ attached
1248 #($ctx->{circ}, $evt) = $U->fetch_open_circulation($copy->id);
1249 ($ctx->{circ}, $evt) = $U->fetch_all_open_circulation($copy->id); # - get ones with stop fines as well
1250 return $evt if ($evt and $__isrenewal); # renewals require a circulation
1252 $circ = $ctx->{circ};
1254 # if the circ is marked as 'claims returned', add the event to the list
1255 push( @eventlist, OpenILS::Event->new('CIRC_CLAIMS_RETURNED') )
1256 if ($circ and $circ->stop_fines and $circ->stop_fines eq 'CLAIMSRETURNED');
1260 if($ctx->{override}) {
1261 $evt = override_events($requestor, $requestor->ws_ou, \@eventlist );
1262 return $evt if $evt;
1268 ($ctx->{transit}) = $U->fetch_open_transit_by_copy($copy->id);
1270 if( $ctx->{circ} ) {
1272 # There is an open circ on this item, close it out.
1274 $evt = _checkin_handle_circ($ctx);
1275 return $evt if $evt;
1277 } elsif( $ctx->{transit} ) {
1279 # is this item currently in transit?
1281 $evt = $transcode->transit_receive( $copy, $requestor, $session );
1282 my $holdtrans = $evt->{holdtransit};
1283 ($ctx->{hold}) = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1285 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1287 # either an error occurred or a ROUTE_ITEM was generated and the
1288 # item must be forwarded on to its destination.
1289 return _checkin_flesh_event($ctx, $evt);
1293 # Transit has been closed, now let's see if the copy's original
1294 # status is something the staff should be warned of
1295 my $e = _checkin_check_copy_status($ctx);
1300 # copy was received as a hold transit. Copy is at target lib
1301 # and hold transit is complete. We're done here...
1302 $U->commit_db_session($session);
1303 return _checkin_flesh_event($ctx, $evt);
1309 # ------------------------------------------------------------------------------
1310 # Circulations and transits are now closed where necessary. Now go on to see if
1311 # this copy can fulfill a hold or needs to be routed to a different location
1312 # ------------------------------------------------------------------------------
1315 # If it's a renewal, we're done
1317 $U->commit_db_session($session);
1318 return OpenILS::Event->new('SUCCESS');
1321 # Now, let's see if this copy is needed for a hold
1322 my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor );
1326 $ctx->{hold} = $hold;
1329 # Capture the hold with this copy
1330 return $evt if ($evt = _checkin_capture_hold($ctx));
1332 if( $hold->pickup_lib == $requestor->ws_ou ) {
1334 # This hold was captured in the correct location
1335 $evt = OpenILS::Event->new('SUCCESS');
1339 # Hold needs to be picked up elsewhere. Build a hold
1340 # transit and route the item.
1341 return $evt if ($evt =_checkin_build_hold_transit($ctx));
1342 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1345 } else { # not needed for a hold
1347 if( $copy->circ_lib == $requestor->ws_ou ) {
1349 # Copy is in the right place.
1350 $evt = OpenILS::Event->new('SUCCESS');
1352 # if the item happens to be a pre-cataloged item, send it
1353 # to cataloging and return the event
1354 my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1355 return $err if $err;
1361 # Copy wants to go home. Transit it there.
1362 return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1363 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1369 # ------------------------------------------------------------------
1370 # if the copy is not in a state that should persist,
1371 # set the copy to reshelving if it's not already there
1372 # ------------------------------------------------------------------
1373 my ($c, $e) = _reshelve_copy($ctx);
1375 $change = $c unless $change;
1379 $evt = OpenILS::Event->new('NO_CHANGE');
1380 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id)
1383 if( $copy->status == $U->copy_status_from_name('on holds shelf')->id );
1387 $U->commit_db_session($session);
1390 $logger->activity("checkin by user ".$requestor->id." on item ".
1391 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1393 return _checkin_flesh_event($ctx, $evt);
1396 sub _reshelve_copy {
1399 my $copy = $ctx->{copy};
1400 my $reqr = $ctx->{requestor};
1401 my $session = $ctx->{session};
1402 my $force = $ctx->{force};
1404 my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1407 $stat != $U->copy_status_from_name('on holds shelf')->id and
1408 $stat != $U->copy_status_from_name('available')->id and
1409 $stat != $U->copy_status_from_name('cataloging')->id and
1410 $stat != $U->copy_status_from_name('in transit')->id and
1411 $stat != $U->copy_status_from_name('reshelving')->id) ) {
1413 $copy->status( $U->copy_status_from_name('reshelving')->id );
1415 my $evt = $U->update_copy(
1417 editor => $reqr->id,
1418 session => $session,
1429 # returns undef if there are no 'open' claims-returned circs attached
1430 # to the given copy. if there is an open claims-returned circ,
1431 # then we check for override mode. if in override, mark the claims-returned
1432 # circ as checked in. if not, return event.
1433 sub _handle_claims_returned {
1435 my $copy = $ctx->{copy};
1437 my $CR = _fetch_open_claims_returned($copy->id);
1438 return undef unless $CR;
1440 # - If the caller has set the override flag, we will check the item in
1441 if($ctx->{override}) {
1443 $CR->checkin_time('now');
1444 $CR->checkin_lib($ctx->{requestor}->ws_ou);
1445 $CR->checkin_staff($ctx->{requestor}->id);
1447 my $stat = $U->storagereq(
1448 'open-ils.storage.direct.action.circulation.update', $CR);
1449 return $U->DB_UPDATE_FAILED($CR) unless $stat;
1450 return OpenILS::Event->new('SUCCESS');
1453 # - if not in override mode, return the CR event
1454 return OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1459 sub _fetch_open_claims_returned {
1461 my $trans = $U->cstorereq(
1462 'open-ils.cstore.direct.action.circulation.search',
1464 target_copy => $copyid,
1465 stop_fines => 'CLAIMSRETURNED',
1466 checkin_time => undef,
1469 return $$trans[0] if $trans && $$trans[0];
1474 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1475 sub _checkin_handle_precat {
1478 my $copy = $ctx->{copy};
1483 my $catstat = $U->copy_status_from_name('cataloging');
1485 if( $ctx->{precat} ) {
1487 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1489 if( $copy->status != $catstat->id ) {
1490 $copy->status($catstat->id);
1492 return (undef, 0, $errevt) if (
1493 $errevt = $U->update_copy(
1495 editor => $ctx->{requestor}->id,
1496 session => $ctx->{session} ));
1502 return ($evt, $change, undef);
1506 # returns the appropriate event for the given copy status
1507 # if the copy is not in a 'special' status, undef is returned
1508 sub _checkin_check_copy_status {
1510 my $copy = $ctx->{copy};
1511 my $reqr = $ctx->{requestor};
1512 my $ses = $ctx->{session};
1518 my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1521 if( $status == $U->copy_status_from_name('available')->id ||
1522 $status == $U->copy_status_from_name('checked out')->id ||
1523 $status == $U->copy_status_from_name('in process')->id ||
1524 $status == $U->copy_status_from_name('in transit')->id ||
1525 $status == $U->copy_status_from_name('reshelving')->id );
1527 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1528 if( $status == $U->copy_status_from_name('lost')->id );
1530 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1531 if( $status == $U->copy_status_from_name('missing')->id );
1533 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1540 # Just gets the copy back home. Returns undef on success, event on error
1541 sub _checkin_build_generic_copy_transit {
1544 my $requestor = $ctx->{requestor};
1545 my $copy = $ctx->{copy};
1546 my $transit = Fieldmapper::action::transit_copy->new;
1547 my $session = $ctx->{session};
1549 $logger->activity("User ". $requestor->id ." creating a ".
1550 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1552 $transit->source($requestor->ws_ou);
1553 $transit->dest($copy->circ_lib);
1554 $transit->target_copy($copy->id);
1555 $transit->source_send_time('now');
1556 $transit->copy_status($copy->status);
1558 $logger->debug("Creating new copy_transit in DB");
1560 my $s = $session->request(
1561 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1562 return $U->DB_UPDATE_FAILED($transit) unless $s;
1564 $logger->info("Checkin copy successfully created new transit: $s");
1566 $copy->status($U->copy_status_from_name('in transit')->id );
1568 return $U->update_copy( copy => $copy,
1569 editor => $requestor->id, session => $session );
1574 # returns event on error, undef on success
1575 sub _checkin_build_hold_transit {
1578 my $copy = $ctx->{copy};
1579 my $hold = $ctx->{hold};
1580 my $trans = Fieldmapper::action::hold_transit_copy->new;
1582 $trans->hold($hold->id);
1583 $trans->source($ctx->{requestor}->ws_ou);
1584 $trans->dest($hold->pickup_lib);
1585 $trans->source_send_time("now");
1586 $trans->target_copy($copy->id);
1587 $trans->copy_status($copy->status);
1589 my $id = $ctx->{session}->request(
1590 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1591 return $U->DB_UPDATE_FAILED($trans) unless $id;
1593 $logger->info("Checkin copy successfully created hold transit: $id");
1595 $copy->status($U->copy_status_from_name('in transit')->id );
1596 return $U->update_copy( copy => $copy,
1597 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1600 # Returns event on error, undef on success
1601 sub _checkin_capture_hold {
1603 my $copy = $ctx->{copy};
1604 my $hold = $ctx->{hold};
1606 $logger->debug("Checkin copy capturing hold ".$hold->id);
1608 $hold->current_copy($copy->id);
1609 $hold->capture_time('now');
1611 my $stat = $ctx->{session}->request(
1612 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1613 return $U->DB_UPDATE_FAILED($hold) unless $stat;
1615 $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1617 return $U->update_copy( copy => $copy,
1618 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1621 # fleshes an event with the relevant objects from the context
1622 sub _checkin_flesh_event {
1627 $payload->{copy} = $U->unflesh_copy($ctx->{copy});
1628 $payload->{record} = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1629 $payload->{circ} = $ctx->{circ} if $ctx->{circ};
1630 $payload->{transit} = $ctx->{transit} if $ctx->{transit};
1631 $payload->{hold} = $ctx->{hold} if $ctx->{hold};
1633 $evt->{payload} = $payload;
1638 # Closes out the circulation, puts the copy into reshelving.
1639 # Voids any bills attached to this circ after the backdate time
1640 # if a backdate is provided
1641 sub _checkin_handle_circ {
1645 my $circ = $ctx->{circ};
1646 my $copy = $ctx->{copy};
1647 my $requestor = $ctx->{requestor};
1648 my $session = $ctx->{session};
1652 $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1654 # backdate the circ if necessary
1655 if(my $backdate = $ctx->{backdate}) {
1656 return $evt if ($evt =
1657 _checkin_handle_backdate($backdate, $circ, $requestor, $session, 1));
1661 if(!$circ->stop_fines) {
1662 $circ->stop_fines('CHECKIN');
1663 $circ->stop_fines('RENEW') if $__isrenewal;
1664 $circ->stop_fines_time('now');
1667 # see if there are any fines owed on this circ. if not, close it
1668 ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1669 return $evt if $evt;
1670 $circ->xact_finish('now') if( $obt->balance_owed == 0 );
1672 # Set the checkin vars since we have the item
1673 $circ->checkin_time('now');
1674 $circ->checkin_staff($requestor->id);
1675 $circ->checkin_lib($requestor->ws_ou);
1677 $evt = _set_copy_reshelving($copy, $requestor->id, $ctx->{session});
1678 return $evt if $evt;
1680 $ctx->{session}->request(
1681 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1686 sub _set_copy_reshelving {
1687 my( $copy, $reqr, $session ) = @_;
1689 $logger->info("Setting copy ".$copy->id." to reshelving");
1690 $copy->status($U->copy_status_from_name('reshelving')->id);
1692 my $evt = $U->update_copy(
1693 session => $session,
1697 return $evt if $evt;
1700 # returns event on error, undef on success
1701 # This voids all bills attached to the given circulation that occurred
1702 # after the backdate
1703 # THIS DOES NOT CLOSE THE CIRC if there are no more fines on the item
1704 sub _checkin_handle_backdate {
1705 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1707 $logger->activity("User ".$requestor->id.
1708 " backdating circ [".$circ->target_copy."] to date: $backdate");
1710 my $bills = $session->request( # XXX Verify this call is correct
1711 "open-ils.storage.direct.money.billing.search_where.atomic",
1712 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1715 for my $bill (@$bills) {
1717 my $n = $bill->note || "";
1718 $bill->note($n . "\nSYSTEM VOIDED FOR BACKDATE");
1719 my $s = $session->request(
1720 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1721 return $U->DB_UPDATE_FAILED($bill) unless $s;
1725 # if the caller elects to attempt to close the circulation
1726 # transaction, then it will be closed if there are not further
1727 # charges on the transaction
1729 #my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1730 #return $evt if $evt;
1731 #$circ->xact_finish($backdate) if $obt->balance_owed <= 0;
1738 sub _find_patron_from_params {
1746 if(my $barcode = $params->{barcode}) {
1747 $logger->debug("circ finding user from params with barcode $barcode");
1748 ($copy, $evt) = $U->fetch_copy_by_barcode($barcode);
1749 return (undef, undef, $evt) if $evt;
1750 ($circ, $evt) = $U->fetch_open_circulation($copy->id);
1751 return (undef, undef, $evt) if $evt;
1752 ($patron, $evt) = $U->fetch_user($circ->usr);
1753 return (undef, undef, $evt) if $evt;
1755 return ($patron, $copy);
1759 # ------------------------------------------------------------------------------
1761 __PACKAGE__->register_method(
1763 api_name => "open-ils.circ.renew.override",
1764 signature => q/@see open-ils.circ.renew/,
1768 __PACKAGE__->register_method(
1770 api_name => "open-ils.circ.renew",
1771 notes => <<" NOTES");
1772 PARAMS( authtoken, circ => circ_id );
1773 open-ils.circ.renew(login_session, circ_object);
1774 Renews the provided circulation. login_session is the requestor of the
1775 renewal and if the logged in user is not the same as circ->usr, then
1776 the logged in user must have RENEW_CIRC permissions.
1780 my( $self, $client, $authtoken, $params ) = @_;
1783 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1786 $params->{override} = 1 if $self->api_name =~ /override/o;
1788 # fetch the patron object one way or another
1789 if( $params->{patron} ) {
1790 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1791 if($evt) { $__isrenewal = 0; return $evt; }
1793 } elsif( $params->{patron_barcode} ) {
1794 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1795 if($evt) { $__isrenewal = 0; return $evt; }
1798 ($patron, $copy, $evt) = _find_patron_from_params($params);
1799 if($evt) { $__isrenewal = 0; return $evt; }
1800 $params->{copy} = $copy;
1803 # verify our login session
1804 ($requestor, $evt) = $U->checkses($authtoken);
1805 if($evt) { $__isrenewal = 0; return $evt; }
1807 # make sure we have permission to perform a renewal
1808 if( $requestor->id ne $patron->id ) {
1809 $evt = $U->check_perms($requestor->id, $requestor->ws_ou, 'RENEW_CIRC');
1810 if($evt) { $__isrenewal = 0; return $evt; }
1814 # fetch and build the circulation environment
1815 ( $ctx, $evt ) = create_circ_ctx( %$params,
1817 requestor => $requestor,
1820 fetch_copy_statuses => 1,
1821 fetch_copy_locations => 1,
1823 if($evt) { $__isrenewal = 0; return $evt; }
1824 $params->{_ctx} = $ctx;
1826 # make sure they have some renewals left and make sure the circulation exists
1827 ($circ, $evt) = _check_renewal_remaining($ctx);
1828 if($evt) { $__isrenewal = 0; return $evt; }
1829 $ctx->{old_circ} = $circ;
1830 my $renewals = $circ->renewal_remaining - 1;
1832 # run the renew permit script
1833 $evt = _run_renew_scripts($ctx);
1834 if($evt) { $__isrenewal = 0; return $evt; }
1837 #$ctx->{patron} = $ctx->{patron}->id;
1838 $evt = $self->generic_receive($client, $authtoken, $ctx );
1839 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1841 if( !$U->event_equals($evt, 'SUCCESS') ) {
1842 $__isrenewal = 0; return $evt;
1845 # re-fetch the context since objects have changed in the checkin
1846 # XXX Do we really need to do this - what changes that we don't control??
1847 ( $ctx, $evt ) = create_circ_ctx( %$params,
1849 requestor => $requestor,
1852 fetch_copy_statuses => 1,
1853 fetch_copy_locations => 1,
1855 if($evt) { $__isrenewal = 0; return $evt; }
1856 $params->{_ctx} = $ctx;
1857 $ctx->{renewal_remaining} = $renewals;
1859 # run the circ permit scripts
1860 if( $ctx->{permit_override} ) {
1861 $evt = $U->check_perms(
1862 $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1863 if($evt) { $__isrenewal = 0; return $evt; }
1866 $evt = $self->permit_circ( $client, $authtoken, $params );
1867 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1868 $params->{precat} = 1;
1871 if(!$U->event_equals($evt, 'SUCCESS')) {
1872 if($evt) { $__isrenewal = 0; return $evt; }
1875 $params->{permit_key} = $evt->{payload};
1879 # checkout the item again
1880 $params->{patron} = $ctx->{patron}->id;
1881 $evt = $self->checkout($client, $authtoken, $params );
1883 $logger->activity("user ".$requestor->id." renewl of item ".
1884 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1890 sub _check_renewal_remaining {
1893 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1894 return (undef, $evt) if $evt;
1895 $evt = OpenILS::Event->new(
1896 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1897 return ($circ, $evt);
1900 sub _run_renew_scripts {
1902 my $runner = $ctx->{runner};
1905 $runner->load($scripts{circ_permit_renew});
1906 my $result = $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1907 my $events = $result->{events};
1909 $logger->activity("circ_permit_renew for user ".
1910 $ctx->{patron}->id." returned events: @$events") if @$events;
1913 push( @allevents, OpenILS::Event->new($_)) for @$events;
1914 return \@allevents if @allevents;