1 package OpenILS::Application::Circ::Circulate;
2 use base 'OpenSRF::Application';
3 use strict; use warnings;
4 use OpenSRF::EX qw(:try);
6 use OpenSRF::Utils::Cache;
7 use OpenSRF::AppSession;
8 use Digest::MD5 qw(md5_hex);
9 use OpenILS::Utils::ScriptRunner;
10 use OpenILS::Application::AppUtils;
11 use OpenILS::Application::Circ::Holds;
12 use OpenILS::Application::Circ::Transit;
13 use OpenILS::Utils::PermitHold;
14 use OpenSRF::Utils::Logger qw(:logger);
16 use DateTime::Format::ISO8601;
17 use OpenSRF::Utils qw/:datetime/;
19 $Data::Dumper::Indent = 0;
20 my $apputils = "OpenILS::Application::AppUtils";
22 my $holdcode = "OpenILS::Application::Circ::Holds";
23 my $transcode = "OpenILS::Application::Circ::Transit";
25 my %scripts; # - circulation script filenames
26 my $script_libs; # - any additional script libraries
27 my %cache; # - db objects cache
28 my %contexts; # - Script runner contexts
29 my $cache_handle; # - memcache handle
31 sub PRECAT_FINE_LEVEL { return 2; }
32 sub PRECAT_LOAN_DURATION { return 2; }
34 my %RECORD_FROM_COPY_CACHE;
37 # for security, this is a process-defined and not
38 # a client-defined variable
41 # ------------------------------------------------------------------------------
42 # Load the circ script from the config
43 # ------------------------------------------------------------------------------
47 $cache_handle = OpenSRF::Utils::Cache->new('global');
48 my $conf = OpenSRF::Utils::SettingsClient->new;
49 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
50 my @pfx = ( @pfx2, "scripts" );
52 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
53 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
54 my $d = $conf->config_value( @pfx, 'circ_duration' );
55 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
56 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
57 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
58 my $lb = $conf->config_value( @pfx2, 'script_path' );
60 $logger->error( "Missing circ script(s)" )
61 unless( $p and $c and $d and $f and $m and $pr );
63 $scripts{circ_permit_patron} = $p;
64 $scripts{circ_permit_copy} = $c;
65 $scripts{circ_duration} = $d;
66 $scripts{circ_recurring_fines}= $f;
67 $scripts{circ_max_fines} = $m;
68 $scripts{circ_permit_renew} = $pr;
70 $lb = [ $lb ] unless ref($lb);
73 $logger->debug("Loaded rules scripts for circ: " .
74 "circ permit patron: $p, circ permit copy: $c, ".
75 "circ duration :$d , circ recurring fines : $f, " .
76 "circ max fines : $m, circ renew permit : $pr");
80 # ------------------------------------------------------------------------------
81 # Loads the necessary circ objects and pushes them into the script environment
82 # Returns ( $data, $evt ). if $evt is defined, then an
83 # unexpedted event occurred and should be dealt with / returned to the caller
84 # ------------------------------------------------------------------------------
92 $evt = _ctx_add_patron_objects($ctx, %params);
93 return (undef,$evt) if $evt;
95 if(!$params{noncat}) {
96 if( $evt = _ctx_add_copy_objects($ctx, %params) ) {
97 $ctx->{precat} = 1 if($evt->{textcode} eq 'ASSET_COPY_NOT_FOUND')
99 $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
103 _doctor_patron_object($ctx) if $ctx->{patron};
104 _doctor_copy_object($ctx) if $ctx->{copy};
106 if(!$ctx->{no_runner}) {
107 _build_circ_script_runner($ctx);
108 _add_script_runner_methods($ctx);
114 sub _ctx_add_patron_objects {
115 my( $ctx, %params) = @_;
118 # - patron standings are now handled in the penalty server...
120 #if(!defined($cache{patron_standings})) {
121 # $cache{patron_standings} = $U->fetch_patron_standings();
123 #$ctx->{patron_standings} = $cache{patron_standings};
125 $cache{group_tree} = $U->fetch_permission_group_tree() unless $cache{group_tree};
126 $ctx->{group_tree} = $cache{group_tree};
128 $ctx->{patron_circ_summary} =
129 $U->fetch_patron_circ_summary($ctx->{patron}->id)
130 if $params{fetch_patron_circsummary};
136 sub _find_copy_by_attr {
141 my $copy = $params{copy} || undef;
146 $U->fetch_copy($params{copyid}) if $params{copyid};
147 return (undef,$evt) if $evt;
151 $U->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
152 return (undef,$evt) if $evt;
155 return ( $copy, $evt );
158 sub _ctx_add_copy_objects {
159 my($ctx, %params) = @_;
164 $cache{copy_statuses} = $U->fetch_copy_statuses
165 if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
167 $cache{copy_locations} = $U->fetch_copy_locations
168 if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
170 $ctx->{copy_statuses} = $cache{copy_statuses};
171 $ctx->{copy_locations} = $cache{copy_locations};
173 ($copy, $evt) = _find_copy_by_attr(%params);
176 if( $copy and !$ctx->{title} ) {
177 $logger->debug("Copy status: " . $copy->status);
179 my $r = $RECORD_FROM_COPY_CACHE{$copy->id};
180 ($r, $evt) = $U->fetch_record_by_copy( $copy->id ) unless $r;
182 $RECORD_FROM_COPY_CACHE{$copy->id} = $r;
185 $ctx->{copy} = $copy;
192 # ------------------------------------------------------------------------------
193 # Fleshes parts of the patron object
194 # ------------------------------------------------------------------------------
195 sub _doctor_copy_object {
198 my $copy = $ctx->{copy} || return undef;
200 $logger->debug("Doctoring copy object...");
202 # set the copy status to a status name
203 $copy->status( _get_copy_status( $copy, $ctx->{copy_statuses} ) );
205 # set the copy location to the location object
206 $copy->location( _get_copy_location( $copy, $ctx->{copy_locations} ) );
208 $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
212 # ------------------------------------------------------------------------------
213 # Fleshes parts of the patron object
214 # ------------------------------------------------------------------------------
215 sub _doctor_patron_object {
218 my $patron = $ctx->{patron} || return undef;
220 # push the standing object into the patron
221 # if(ref($ctx->{patron_standings})) {
222 # for my $s (@{$ctx->{patron_standings}}) {
223 # if( $s->id eq $ctx->{patron}->standing ) {
224 # $patron->standing($s);
225 # $logger->debug("Set patron standing to ". $s->value);
230 # set the patron ptofile to the profile name
231 $patron->profile( _get_patron_profile(
232 $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
236 $U->fetch_org_unit( $patron->home_ou ) ) if $patron;
240 # recurse and find the patron profile name from the tree
241 # another option would be to grab the groups for the patron
242 # and cycle through those until the "profile" group has been found
243 sub _get_patron_profile {
244 my( $patron, $group_tree ) = @_;
245 return $group_tree if ($group_tree->id eq $patron->profile);
246 return undef unless ($group_tree->children);
248 for my $child (@{$group_tree->children}) {
249 my $ret = _get_patron_profile( $patron, $child );
255 sub _get_copy_status {
256 my( $copy, $cstatus ) = @_;
259 for my $status (@$cstatus) {
260 $s = $status if( $status->id eq $copy->status )
262 $logger->debug("Retrieving copy status: " . $s->name) if $s;
266 sub _get_copy_location {
267 my( $copy, $locations ) = @_;
270 for my $loc (@$locations) {
271 $l = $loc if $loc->id eq $copy->location;
273 $logger->debug("Retrieving copy location: " . $l->name ) if $l;
278 # ------------------------------------------------------------------------------
279 # Constructs and shoves data into the script environment
280 # ------------------------------------------------------------------------------
281 sub _build_circ_script_runner {
285 $logger->debug("Loading script environment for circulation");
288 if( $runner = $contexts{$ctx->{type}} ) {
289 $runner->refresh_context;
291 $runner = OpenILS::Utils::ScriptRunner->new;
292 $contexts{type} = $runner;
296 $logger->debug("Loading circ script lib path $_");
297 $runner->add_path( $_ );
300 # Note: inserting the number 0 into the script turns into the
301 # string "0", and thus evaluates to true in JS land
302 # inserting undef will insert "", which evaluates to false
304 $runner->insert( 'environment.patron', $ctx->{patron}, 1);
305 $runner->insert( 'environment.title', $ctx->{title}, 1);
306 $runner->insert( 'environment.copy', $ctx->{copy}, 1);
309 $runner->insert( 'result', {} );
310 #$runner->insert( 'result.event', 'SUCCESS' );
311 $runner->insert( 'result.events', [] );
314 $runner->insert('environment.isRenewal', 1);
316 $runner->insert('environment.isRenewal', undef);
319 if($ctx->{ishold} ) {
320 $runner->insert('environment.isHold', 1);
322 $runner->insert('environment.isHold', undef)
325 if( $ctx->{noncat} ) {
326 $runner->insert('environment.isNonCat', 1);
327 $runner->insert('environment.nonCatType', $ctx->{noncat_type});
329 $runner->insert('environment.isNonCat', undef);
332 # if(ref($ctx->{patron_circ_summary})) {
333 # $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
334 # $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
337 $ctx->{runner} = $runner;
342 sub _add_script_runner_methods {
345 my $runner = $ctx->{runner};
349 # allows a script to fetch a hold that is currently targeting the
351 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
353 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
354 $hold = undef unless $hold;
355 $runner->insert( $key, $hold, 1 );
361 # ------------------------------------------------------------------------------
363 __PACKAGE__->register_method(
364 method => "permit_circ",
365 api_name => "open-ils.circ.checkout.permit",
367 Determines if the given checkout can occur
368 @param authtoken The login session key
369 @param params A trailing hash of named params including
370 barcode : The copy barcode,
371 patron : The patron the checkout is occurring for,
372 renew : true or false - whether or not this is a renewal
373 @return The event that occurred during the permit check.
376 __PACKAGE__->register_method (
377 method => 'permit_circ',
378 api_name => 'open-ils.circ.checkout.permit.override',
379 signature => q/@see open-ils.circ.checkout.permit/,
383 my( $self, $client, $authtoken, $params ) = @_;
386 my $override = $params->{override} = 1 if $self->api_name =~ /override/o;
388 my ( $requestor, $patron, $ctx, $evt, $circ );
390 # check permisson of the requestor
391 ( $requestor, $patron, $evt ) =
392 $U->checkses_requestor(
393 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
397 # fetch and build the circulation environment
398 if( !( $ctx = $params->{_ctx}) ) {
400 ( $ctx, $evt ) = create_circ_ctx( %$params,
402 requestor => $requestor,
404 #fetch_patron_circ_summary => 1,
405 fetch_copy_statuses => 1,
406 fetch_copy_locations => 1,
411 my $copy = $ctx->{copy};
413 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
414 return OpenILS::Event->new('COPY_IN_TRANSIT')
415 if $stat == $U->copy_status_from_name('in transit')->id;
418 $ctx->{authtoken} = $authtoken;
421 if( $ctx->{copy} and ($evt = _handle_claims_returned($ctx)) ) {
422 return $evt unless $U->event_equals($evt, 'SUCCESS');
430 # no claims returned circ was found, check if there is any open circ
431 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
432 ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
433 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
438 $ctx->{permit_key} = _cache_permit_key();
439 my $events = _run_permit_scripts($ctx);
442 $evt = override_events($requestor, $requestor->ws_ou,
443 $events, $authtoken, $ctx->{copy}->id, $client);
445 return OpenILS::Event->new('SUCCESS', payload => $ctx->{permit_key} );
451 sub override_events {
453 my( $requestor, $org, $events, $authtoken, $copyid, $conn ) = @_;
454 $events = [ $events ] unless ref($events) eq 'ARRAY';
457 for my $e (@$events) {
458 my $tc = $e->{textcode};
459 next if $tc eq 'SUCCESS';
460 my $ov = "$tc.override";
461 $logger->info("attempting to override event $ov");
462 my $evt = $U->check_perms( $requestor->id, $org, $ov );
470 __PACKAGE__->register_method(
471 method => "check_title_hold",
472 api_name => "open-ils.circ.title_hold.is_possible",
474 Determines if a hold were to be placed by a given user,
475 whether or not said hold would have any potential copies
477 @param authtoken The login session key
478 @param params A hash of named params including:
479 patronid - the id of the hold recipient
480 titleid (brn) - the id of the title to be held
481 depth - the hold range depth (defaults to 0)
484 # XXX add pickup lib to the call to test for perms
486 sub check_title_hold {
487 my( $self, $client, $authtoken, $params ) = @_;
488 my %params = %$params;
489 my $titleid = $params{titleid};
491 my ( $requestor, $patron, $evt ) = $U->checkses_requestor(
492 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
495 my $rangelib = $patron->home_ou;
496 my $depth = $params{depth} || 0;
497 my $pickup = $params{pickup_lib};
499 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
501 my $org = $U->simplereq(
503 'open-ils.actor.org_unit.retrieve',
504 $authtoken, $requestor->home_ou );
510 while( $title = $U->storagereq(
511 'open-ils.storage.biblio.record_entry.ranged_tree',
512 $titleid, $rangelib, $depth, $limit, $offset ) ) {
516 ref($title->call_numbers) and
517 @{$title->call_numbers};
519 for my $cn (@{$title->call_numbers}) {
521 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
523 for my $copy (@{$cn->copies}) {
525 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
527 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
529 requestor => $requestor,
532 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
533 pickup_lib => $pickup,
534 request_lib => $org } );
536 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
547 # Runs the patron and copy permit scripts
548 # if this is a non-cat circulation, the copy permit script
550 sub _run_permit_scripts {
553 my $runner = $ctx->{runner};
554 my $patronid = $ctx->{patron}->id;
555 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
556 my $key = $ctx->{permit_key};
560 # ---------------------------------------------------------------------
561 # Find all of the fatal penalties currently set on the user
562 # ---------------------------------------------------------------------
563 my $penalties = $U->update_patron_penalties(
564 authtoken => $ctx->{authtoken},
565 patron => $ctx->{patron}
568 $penalties = $penalties->{fatal_penalties};
569 $logger->info("circ patron penalties user $patronid: @$penalties");
572 # ---------------------------------------------------------------------
573 # Now run the patron permit script
574 # ---------------------------------------------------------------------
575 $runner->load($scripts{circ_permit_patron});
576 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
578 my $patron_events = $runner->retrieve('result.events');
579 $patron_events = [ split(/,/, $patron_events) ];
580 $ctx->{circ_permit_patron_events} = $patron_events;
581 $logger->activity("circ_permit_patron for returned @$patron_events") if @$patron_events;
583 my @evts_so_far = (@$penalties, @$patron_events);
585 return \@evts_so_far if @evts_so_far;
588 if( $ctx->{noncat} ) {
589 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
590 return OpenILS::Event->new('SUCCESS', payload => $key);
594 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
595 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
599 $logger->debug("Exiting circ permit early because request is for hold patron permit");
600 return OpenILS::Event->new('SUCCESS');
605 # ---------------------------------------------------------------------
606 # Capture all of the copy permit events
607 # ---------------------------------------------------------------------
608 $runner->load($scripts{circ_permit_copy});
609 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
611 my $copy_events = $runner->retrieve('result.events');
612 $copy_events = [ split(/,/, $copy_events) ];
613 $ctx->{circ_permit_copy_events} = $copy_events;
614 $logger->activity("circ_permit_copy for copy ".
615 "$barcode returned events: @$copy_events") if @$copy_events;
620 # ---------------------------------------------------------------------
621 # Now collect all of the events together
622 # ---------------------------------------------------------------------
623 my @allevents = ( @evts_so_far, @$copy_events );
625 #push( @allevents, OpenILS::Event->new($_)) for @$penalties;
626 #push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
628 my $ae = _check_copy_alert($ctx->{copy});
629 push( @allevents, $ae ) if $ae;
631 return OpenILS::Event->new('SUCCESS', payload => $key) unless (@allevents);
634 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
635 @allevents = values %hash;
638 $_->{payload} = $ctx->{copy}->status->id
639 if ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
645 sub _check_copy_alert {
647 return OpenILS::Event->new('COPY_ALERT_MESSAGE',
648 payload => $copy->alert_message) if $copy->alert_message;
652 # takes copyid, patronid, and requestor id
653 sub _cache_permit_key {
654 my $key = md5_hex( time() . rand() . "$$" );
655 $logger->debug("Setting circ permit key to $key");
656 $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
660 sub _check_permit_key {
662 $logger->debug("Fetching circ permit key $key");
663 my $k = "oils_permit_key_$key";
664 my $one = $cache_handle->get_cache($k);
665 $cache_handle->delete_cache($k);
666 return ($one) ? 1 : 0;
670 # ------------------------------------------------------------------------------
672 __PACKAGE__->register_method(
673 method => "checkout",
674 api_name => "open-ils.circ.checkout",
677 @param authtoken The login session key
678 @param params A named hash of params including:
680 barcode If no copy is provided, the copy is retrieved via barcode
681 copyid If no copy or barcode is provide, the copy id will be use
682 patron The patron's id
683 noncat True if this is a circulation for a non-cataloted item
684 noncat_type The non-cataloged type id
685 noncat_circ_lib The location for the noncat circ.
686 precat The item has yet to be cataloged
687 dummy_title The temporary title of the pre-cataloded item
688 dummy_author The temporary authr of the pre-cataloded item
689 Default is the home org of the staff member
690 @return The SUCCESS event on success, any other event depending on the error
694 my( $self, $client, $authtoken, $params ) = @_;
697 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
698 my $key = $params->{permit_key};
700 # if this is a renewal, then the requestor does not have to
701 # have checkout privelages
702 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
703 ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
706 if( $params->{patron} ) {
707 ( $patron, $evt ) = $U->fetch_user($params->{patron});
710 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
714 # set the circ lib to the home org of the requestor if not specified
715 my $circlib = (defined($params->{circ_lib})) ?
716 $params->{circ_lib} : $requestor->ws_ou;
719 # Make sure the caller has a valid permit key or is
720 # overriding the permit can
721 if( $params->{permit_override} ) {
722 $evt = $U->check_perms(
723 $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
727 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
728 unless _check_permit_key($key);
731 # if this is a non-cataloged item, check it out and return
732 return _checkout_noncat(
733 $key, $requestor, $patron, %$params ) if $params->{noncat};
735 # if this item has yet to be cataloged, make sure a dummy copy exists
736 ( $params->{copy}, $evt ) = _make_precat_copy(
737 $requestor, $circlib, $params ) if $params->{precat};
741 # fetch and build the circulation environment
742 if( !( $ctx = $params->{_ctx}) ) {
743 ( $ctx, $evt ) = create_circ_ctx( %$params,
745 requestor => $requestor,
746 session => $U->start_db_session(),
748 #fetch_patron_circ_summary => 1,
749 fetch_copy_statuses => 1,
750 fetch_copy_locations => 1,
754 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
756 # if the call doesn't know it's not cataloged..
757 if(!$params->{precat}) {
758 if( $ctx->{copy}->call_number eq '-1' ) {
759 return OpenILS::Event->new('ITEM_NOT_CATALOGED');
764 $copy = $ctx->{copy};
766 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
767 return OpenILS::Event->new('COPY_IN_TRANSIT')
768 if $stat == $U->copy_status_from_name('in transit')->id;
771 # this happens in permit.. but we need to check here for 'offline' requests
772 ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
773 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
775 my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
778 $ctx->{circ_lib} = $circlib;
780 $evt = _run_checkout_scripts($ctx);
784 _build_checkout_circ_object($ctx);
786 $evt = _apply_modified_due_date($ctx);
789 $evt = _commit_checkout_circ_object($ctx);
792 $evt = _update_checkout_copy($ctx);
796 ($holds, $evt) = _handle_related_holds($ctx);
800 $logger->debug("Checkout committing objects with session thread trace: ".$ctx->{session}->session_id);
801 $U->commit_db_session($ctx->{session});
802 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
804 $logger->activity("user ".$requestor->id." successfully checked out item ".
805 $ctx->{copy}->barcode." to user ".$ctx->{patron}->id );
808 # ------------------------------------------------------------------------------
809 # Update the patron penalty info in the DB
810 # ------------------------------------------------------------------------------
811 $U->update_patron_penalties(
812 authtoken => $authtoken,
813 patron => $ctx->{patron} ,
817 return OpenILS::Event->new('SUCCESS',
819 copy => $U->unflesh_copy($ctx->{copy}),
820 circ => $ctx->{circ},
822 holds_fulfilled => $holds,
828 sub _make_precat_copy {
829 my ( $requestor, $circlib, $params ) = @_;
831 my( $copy, undef ) = _find_copy_by_attr(%$params);
834 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
836 $copy->editor($requestor->id);
837 $copy->edit_date('now');
838 $copy->dummy_title($params->{dummy_title});
839 $copy->dummy_author($params->{dummy_author});
841 my $stat = $U->storagereq(
842 'open-ils.storage.direct.asset.copy.update', $copy );
844 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $stat;
848 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
850 my $evt = OpenILS::Event->new(
851 'BAD_PARAMS', desc => "Dummy title or author not provided" )
852 unless ( $params->{dummy_title} and $params->{dummy_author} );
853 return (undef, $evt) if $evt;
855 $copy = Fieldmapper::asset::copy->new;
856 $copy->circ_lib($circlib);
857 $copy->creator($requestor->id);
858 $copy->editor($requestor->id);
859 $copy->barcode($params->{barcode});
860 $copy->call_number(-1); #special CN for precat materials
861 $copy->loan_duration(&PRECAT_LOAN_DURATION);
862 $copy->fine_level(&PRECAT_FINE_LEVEL);
864 $copy->dummy_title($params->{dummy_title});
865 $copy->dummy_author($params->{dummy_author});
867 my $id = $U->storagereq(
868 'open-ils.storage.direct.asset.copy.create', $copy );
869 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
871 $logger->debug("Pre-cataloged copy successfully created");
872 return ($U->fetch_copy($id));
876 sub _run_checkout_scripts {
882 my $runner = $ctx->{runner};
884 $runner->insert('result.durationLevel');
885 $runner->insert('result.durationRule');
886 $runner->insert('result.recurringFinesRule');
887 $runner->insert('result.recurringFinesLevel');
888 $runner->insert('result.maxFine');
890 $runner->load($scripts{circ_duration});
891 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
892 my $duration = $runner->retrieve('result.durationRule');
893 $logger->debug("Circ duration script yielded a duration rule of: $duration");
895 $runner->load($scripts{circ_recurring_fines});
896 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
897 my $recurring = $runner->retrieve('result.recurringFinesRule');
898 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
900 $runner->load($scripts{circ_max_fines});
901 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
902 my $max_fine = $runner->retrieve('result.maxFine');
903 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
905 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
907 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
909 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
912 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
913 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
914 $ctx->{duration_rule} = $duration;
915 $ctx->{recurring_fines_rule} = $recurring;
916 $ctx->{max_fine_rule} = $max_fine;
921 sub _build_checkout_circ_object {
925 my $circ = new Fieldmapper::action::circulation;
926 my $duration = $ctx->{duration_rule};
927 my $max = $ctx->{max_fine_rule};
928 my $recurring = $ctx->{recurring_fines_rule};
929 my $copy = $ctx->{copy};
930 my $patron = $ctx->{patron};
931 my $dur_level = $ctx->{duration_level};
932 my $rec_level = $ctx->{recurring_fines_level};
934 $circ->duration( $duration->shrt ) if ($dur_level == 1);
935 $circ->duration( $duration->normal ) if ($dur_level == 2);
936 $circ->duration( $duration->extended ) if ($dur_level == 3);
938 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
939 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
940 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
942 $circ->duration_rule( $duration->name );
943 $circ->recuring_fine_rule( $recurring->name );
944 $circ->max_fine_rule( $max->name );
945 $circ->max_fine( $max->amount );
947 $circ->fine_interval($recurring->recurance_interval);
948 $circ->renewal_remaining( $duration->max_renewals );
949 $circ->target_copy( $copy->id );
950 $circ->usr( $patron->id );
951 $circ->circ_lib( $ctx->{circ_lib} );
954 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
955 $circ->opac_renewal(1);
956 $circ->renewal_remaining($ctx->{renewal_remaining});
957 $circ->circ_staff($ctx->{requestor}->id);
961 # if the user provided an overiding checkout time,
962 # (e.g. the checkout really happened several hours ago), then
963 # we apply that here. Does this need a perm??
964 if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
965 $logger->debug("circ setting checkout_time to $ds");
966 $circ->xact_start($ds);
969 # if a patron is renewing, 'requestor' will be the patron
970 $circ->circ_staff($ctx->{requestor}->id );
971 _set_circ_due_date($circ);
972 $ctx->{circ} = $circ;
975 sub _apply_modified_due_date {
977 my $circ = $ctx->{circ};
981 if( $ctx->{due_date} ) {
983 my $evt = $U->check_perms(
984 $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
987 my $ds = _create_date_stamp($ctx->{due_date});
988 $logger->debug("circ modifying due_date to $ds");
989 $circ->due_date($ds);
993 # if the due_date lands on a day when the location is closed
994 my $copy = $ctx->{copy};
997 $logger->info("circ searching for closed date overlap on lib ".
998 $copy->circ_lib->id ." with an item due date of ".$circ->due_date );
1000 my $dateinfo = $ctx->{session}->request(
1001 'open-ils.storage.actor.org_unit.closed_date.overlap',
1002 $copy->circ_lib->id, $circ->due_date )->gather(1);
1006 $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
1007 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1009 # XXX make the behavior more dynamic
1010 # for now, we just push the due date to after the close date
1011 $circ->due_date($dateinfo->{end});
1018 sub _create_date_stamp {
1019 my $datestring = shift;
1020 return undef unless $datestring;
1021 $datestring = clense_ISO8601($datestring);
1022 $logger->debug("circ created date stamp => $datestring");
1026 sub _create_due_date {
1027 my $duration = shift;
1029 my ($sec,$min,$hour,$mday,$mon,$year) =
1030 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1031 $year += 1900; $mon += 1;
1032 my $due_date = sprintf(
1033 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1034 $year, $mon, $mday, $hour, $min, $sec);
1038 sub _set_circ_due_date {
1041 my $dd = _create_due_date($circ->duration);
1042 $logger->debug("Checkout setting due date on circ to: $dd");
1043 $circ->due_date($dd);
1046 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
1047 sub _update_checkout_copy {
1050 my $copy = $ctx->{copy};
1052 my $s = $U->copy_status_from_name('checked out');
1053 $copy->status( $s->id ) if $s;
1055 my $evt = $U->update_copy( session => $ctx->{session},
1056 copy => $copy, editor => $ctx->{requestor}->id );
1057 return (undef,$evt) if $evt;
1062 # commits the circ object to the db then fleshes the circ with rules objects
1063 sub _commit_checkout_circ_object {
1066 my $circ = $ctx->{circ};
1070 my $r = $ctx->{session}->request(
1071 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
1073 return $U->DB_UPDATE_FAILED($circ) unless $r;
1075 $logger->debug("Created a new circ object in checkout: $r");
1078 $circ->duration_rule($ctx->{duration_rule});
1079 $circ->max_fine_rule($ctx->{max_fine_rule});
1080 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
1086 # sees if there are any holds that this copy
1087 sub _handle_related_holds {
1090 my $copy = $ctx->{copy};
1091 my $patron = $ctx->{patron};
1092 my $holds = $holdcode->fetch_related_holds($copy->id);
1096 # XXX We should only fulfill one hold here...
1097 # XXX If a hold was transited to the user who is checking out
1098 # the item, we need to make sure that hold is what's grabbed
1099 if(ref($holds) && @$holds) {
1101 # for now, just sort by id to get what should be the oldest hold
1102 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1103 my @myholds = grep { $_->usr eq $patron->id } @$holds;
1104 my @altholds = grep { $_->usr ne $patron->id } @$holds;
1107 my $hold = $myholds[0];
1109 $logger->debug("Related hold found in checkout: " . $hold->id );
1111 $hold->current_copy($copy->id); # just make sure it's set
1112 # if the hold was never officially captured, capture it.
1113 $hold->capture_time('now') unless $hold->capture_time;
1114 $hold->fulfillment_time('now');
1115 my $r = $ctx->{session}->request(
1116 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
1117 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
1118 push( @fulfilled, $hold->id );
1121 # If there are any holds placed for other users that point to this copy,
1122 # then we need to un-target those holds so the targeter can pick a new copy
1125 $logger->info("Un-targeting hold ".$_->id.
1126 " because copy ".$copy->id." is getting checked out");
1128 $_->clear_current_copy;
1129 my $r = $ctx->{session}->request(
1130 "open-ils.storage.direct.action.hold_request.update", $_ )->gather(1);
1131 return (undef,$U->DB_UPDATE_FAILED( $_ )) unless $r;
1135 return (\@fulfilled, undef);
1138 sub _checkout_noncat {
1139 my ( $key, $requestor, $patron, %params ) = @_;
1140 my( $circ, $circlib, $evt );
1143 $circlib = $params{noncat_circ_lib} || $requestor->ws_ou;
1145 my $count = $params{noncat_count} || 1;
1146 my $cotime = _create_date_stamp($params{checkout_time}) || "";
1147 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1149 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1150 $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
1151 return $evt if $evt;
1154 return OpenILS::Event->new(
1155 'SUCCESS', payload => { noncat_circ => $circ } );
1159 __PACKAGE__->register_method(
1160 method => "generic_receive",
1161 api_name => "open-ils.circ.checkin",
1164 Generic super-method for handling all copies
1165 @param authtoken The login session key
1166 @param params Hash of named parameters including:
1167 barcode - The copy barcode
1168 force - If true, copies in bad statuses will be checked in and give good statuses
1173 __PACKAGE__->register_method(
1174 method => "generic_receive",
1175 api_name => "open-ils.circ.checkin.override",
1176 signature => q/@see open-ils.circ.checkin/
1179 sub generic_receive {
1180 my( $self, $conn, $authtoken, $params ) = @_;
1181 my( $ctx, $requestor, $evt );
1183 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1184 ( $requestor, $evt ) = $U->checksesperm(
1185 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1186 return $evt if $evt;
1188 # load up the circ objects
1189 if( !( $ctx = $params->{_ctx}) ) {
1190 ( $ctx, $evt ) = create_circ_ctx( %$params,
1191 requestor => $requestor,
1192 session => $U->start_db_session(),
1194 fetch_copy_statuses => 1,
1195 fetch_copy_locations => 1,
1198 return $evt if $evt;
1200 $ctx->{override} = 1 if $self->api_name =~ /override/o;
1201 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1202 $ctx->{authtoken} = $authtoken;
1203 my $session = $ctx->{session};
1205 my $copy = $ctx->{copy};
1206 $U->unflesh_copy($copy);
1207 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $copy;
1209 $logger->info("Checkin copy called by user ".
1210 $requestor->id." for copy ".$copy->id);
1212 # ------------------------------------------------------------------------------
1213 # Update the patron penalty info in the DB
1214 # ------------------------------------------------------------------------------
1215 $U->update_patron_penalties(
1216 authtoken => $authtoken,
1217 patron => $ctx->{patron},
1221 return $self->checkin_do_receive($conn, $ctx);
1224 sub checkin_do_receive {
1226 my( $self, $connection, $ctx ) = @_;
1229 my $copy = $ctx->{copy};
1230 my $session = $ctx->{session};
1231 my $requestor = $ctx->{requestor};
1232 my $change = 0; # did we actually do anything?
1237 # does the copy have an attached alert message?
1238 my $ae = _check_copy_alert($copy);
1239 push(@eventlist, $ae) if $ae;
1241 # is the copy is an a status we can't automatically resolve?
1242 $evt = _checkin_check_copy_status($ctx);
1243 push( @eventlist, $evt ) if $evt;
1246 # - see if the copy has an open circ attached
1247 ($ctx->{circ}, $evt) = $U->fetch_open_circulation($copy->id);
1248 return $evt if ($evt and $__isrenewal); # renewals require a circulation
1250 $circ = $ctx->{circ};
1252 # if the circ is marked as 'claims returned', add the event to the list
1253 push( @eventlist, 'CIRC_CLAIMS_RETURNED' )
1254 if ($circ and $circ->stop_fines and $circ->stop_fines eq 'CLAIMSRETURNED');
1258 if($ctx->{override}) {
1259 $evt = override_events($requestor, $requestor->ws_ou, \@eventlist );
1260 return $evt if $evt;
1266 ($ctx->{transit}) = $U->fetch_open_transit_by_copy($copy->id);
1268 if( $ctx->{circ} ) {
1270 # There is an open circ on this item, close it out.
1272 $evt = _checkin_handle_circ($ctx);
1273 return $evt if $evt;
1275 } elsif( $ctx->{transit} ) {
1277 # is this item currently in transit?
1279 $evt = $transcode->transit_receive( $copy, $requestor, $session );
1280 my $holdtrans = $evt->{holdtransit};
1281 ($ctx->{hold}) = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1283 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1285 # either an error occurred or a ROUTE_ITEM was generated and the
1286 # item must be forwarded on to its destination.
1287 return _checkin_flesh_event($ctx, $evt);
1291 # Transit has been closed, now let's see if the copy's original
1292 # status is something the staff should be warned of
1293 my $e = _checkin_check_copy_status($ctx);
1298 # copy was received as a hold transit. Copy is at target lib
1299 # and hold transit is complete. We're done here...
1300 $U->commit_db_session($session);
1301 return _checkin_flesh_event($ctx, $evt);
1307 # ------------------------------------------------------------------------------
1308 # Circulations and transits are now closed where necessary. Now go on to see if
1309 # this copy can fulfill a hold or needs to be routed to a different location
1310 # ------------------------------------------------------------------------------
1313 # If it's a renewal, we're done
1316 #my ($cc, $ee) = _reshelve_copy($ctx);
1318 #delete $$ctx{force};
1319 $U->commit_db_session($session);
1320 return OpenILS::Event->new('SUCCESS');
1323 # Now, let's see if this copy is needed for a hold
1324 my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor );
1328 $ctx->{hold} = $hold;
1331 # Capture the hold with this copy
1332 return $evt if ($evt = _checkin_capture_hold($ctx));
1334 if( $hold->pickup_lib == $requestor->ws_ou ) {
1336 # This hold was captured in the correct location
1337 $evt = OpenILS::Event->new('SUCCESS');
1341 # Hold needs to be picked up elsewhere. Build a hold
1342 # transit and route the item.
1343 return $evt if ($evt =_checkin_build_hold_transit($ctx));
1344 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1347 } else { # not needed for a hold
1349 if( $copy->circ_lib == $requestor->ws_ou ) {
1351 # Copy is in the right place.
1352 $evt = OpenILS::Event->new('SUCCESS');
1354 # if the item happens to be a pre-cataloged item, send it
1355 # to cataloging and return the event
1356 my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1357 return $err if $err;
1363 # Copy wants to go home. Transit it there.
1364 return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1365 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1371 # ------------------------------------------------------------------
1372 # if the copy is not in a state that should persist,
1373 # set the copy to reshelving if it's not already there
1374 # ------------------------------------------------------------------
1375 my ($c, $e) = _reshelve_copy($ctx);
1377 $change = $c unless $change;
1381 $evt = OpenILS::Event->new('NO_CHANGE');
1382 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id)
1385 if( $copy->status == $U->copy_status_from_name('on holds shelf')->id );
1389 $U->commit_db_session($session);
1392 $logger->activity("checkin by user ".$requestor->id." on item ".
1393 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1395 return _checkin_flesh_event($ctx, $evt);
1398 sub _reshelve_copy {
1401 my $copy = $ctx->{copy};
1402 my $reqr = $ctx->{requestor};
1403 my $session = $ctx->{session};
1404 my $force = $ctx->{force};
1406 my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1409 $stat != $U->copy_status_from_name('on holds shelf')->id and
1410 $stat != $U->copy_status_from_name('available')->id and
1411 $stat != $U->copy_status_from_name('cataloging')->id and
1412 $stat != $U->copy_status_from_name('in transit')->id and
1413 $stat != $U->copy_status_from_name('reshelving')->id) ) {
1415 $copy->status( $U->copy_status_from_name('reshelving')->id );
1417 my $evt = $U->update_copy(
1419 editor => $reqr->id,
1420 session => $session,
1431 # returns undef if there are no 'open' claims-returned circs attached
1432 # to the given copy. if there is an open claims-returned circ,
1433 # then we check for override mode. if in override, mark the claims-returned
1434 # circ as checked in. if not, return event.
1435 sub _handle_claims_returned {
1437 my $copy = $ctx->{copy};
1439 my $CR = _fetch_open_claims_returned($copy->id);
1440 return undef unless $CR;
1442 # - If the caller has set the override flag, we will check the item in
1443 if($ctx->{override}) {
1445 $CR->checkin_time('now');
1446 $CR->checkin_lib($ctx->{requestor}->ws_ou);
1447 $CR->checkin_staff($ctx->{requestor}->id);
1449 my $stat = $U->storagereq(
1450 'open-ils.storage.direct.action.circulation.update', $CR);
1451 return $U->DB_UPDATE_FAILED($CR) unless $stat;
1452 return OpenILS::Event->new('SUCCESS');
1455 # - if not in override mode, return the CR event
1456 return OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1461 sub _fetch_open_claims_returned {
1463 my $trans = $U->storagereq(
1464 'open-ils.storage.direct.action.circulation.search_where',
1466 target_copy => $copyid,
1467 stop_fines => 'CLAIMSRETURNED',
1468 checkin_time => undef,
1471 return $$trans[0] if $trans && $$trans[0];
1475 # - if the copy is has the 'in process' status, set it to reshelving
1476 #sub _check_in_process {
1479 #my $copy = $ctx->{copy};
1480 #my $reqr = $ctx->{requestor};
1481 #my $ses = $ctx->{session};
1483 #my $stat = $U->copy_status_from_name('in process');
1484 #my $rstat = $U->copy_status_from_name('reshelving');
1486 #if( $stat->id == $copy->status->id ) {
1487 #$logger->info("marking 'in-process' copy ".$copy->id." as 'reshelving'");
1488 #$copy->status( $rstat->id );
1489 #my $evt = $U->update_copy(
1491 #editor => $reqr->id,
1494 #return $evt if $evt;
1496 #$copy->status( $rstat ); # - reflesh the copy status
1502 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1503 sub _checkin_handle_precat {
1506 my $copy = $ctx->{copy};
1511 my $catstat = $U->copy_status_from_name('cataloging');
1513 if( $ctx->{precat} ) {
1515 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1517 if( $copy->status != $catstat->id ) {
1518 $copy->status($catstat->id);
1520 return (undef, 0, $errevt) if (
1521 $errevt = $U->update_copy(
1523 editor => $ctx->{requestor}->id,
1524 session => $ctx->{session} ));
1530 return ($evt, $change, undef);
1534 # returns the appropriate event for the given copy status
1535 # if the copy is not in a 'special' status, undef is returned
1536 sub _checkin_check_copy_status {
1538 my $copy = $ctx->{copy};
1539 my $reqr = $ctx->{requestor};
1540 my $ses = $ctx->{session};
1546 my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1549 if( $status == $U->copy_status_from_name('available')->id ||
1550 $status == $U->copy_status_from_name('checked out')->id ||
1551 $status == $U->copy_status_from_name('in process')->id ||
1552 $status == $U->copy_status_from_name('in transit')->id ||
1553 $status == $U->copy_status_from_name('reshelving')->id );
1555 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1556 if( $status == $U->copy_status_from_name('lost')->id );
1558 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1559 if( $status == $U->copy_status_from_name('missing')->id );
1561 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1565 # my $rstat = $U->copy_status_from_name('reshelving');
1566 # my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1568 # if( $stat == $U->copy_status_from_name('lost')->id ) {
1570 # $evt = OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy );
1572 # } elsif( $stat == $U->copy_status_from_name('missing')->id) {
1574 # $evt = OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy );
1577 # return (undef,$evt) if(!$ctx->{override});
1579 # # we're are now going to attempt to override the failure
1580 # # and set the copy to reshelving
1582 # my $copyid = $copy->id;
1583 # my $userid = $reqr->id;
1586 # # - make sure we have permission
1587 # $e = $U->check_perms( $reqr->id,
1588 # $copy->circ_lib, 'COPY_STATUS_LOST.override');
1589 # return (undef,$e) if $e;
1590 # $copy->status( $rstat->id );
1592 # # XXX if no fines are owed in the circ, close it out - will this happen later anyway?
1593 # #my $circ = $U->storagereq(
1594 # # 'open-ils.storage.direct.action.circulation
1596 # $logger->activity("user $userid overriding 'lost' copy status for copy $copyid");
1598 # } elsif( $ismissing ) {
1600 # # - make sure we have permission
1601 # $e = $U->check_perms( $reqr->id,
1602 # $copy->circ_lib, 'COPY_STATUS_MISSING.override');
1603 # return (undef,$e) if $e;
1604 # $copy->status( $rstat->id );
1605 # $logger->activity("user $userid overriding 'missing' copy status for copy $copyid");
1608 # if( $islost or $ismissing ) {
1610 # # - update the copy with the new status
1611 # $evt = $U->update_copy(
1613 # editor => $reqr->id,
1616 # return (undef,$evt) if $evt;
1617 # $copy->status( $rstat );
1625 # Just gets the copy back home. Returns undef on success, event on error
1626 sub _checkin_build_generic_copy_transit {
1629 my $requestor = $ctx->{requestor};
1630 my $copy = $ctx->{copy};
1631 my $transit = Fieldmapper::action::transit_copy->new;
1632 my $session = $ctx->{session};
1634 $logger->activity("User ". $requestor->id ." creating a ".
1635 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1637 $transit->source($requestor->ws_ou);
1638 $transit->dest($copy->circ_lib);
1639 $transit->target_copy($copy->id);
1640 $transit->source_send_time('now');
1641 $transit->copy_status($copy->status);
1643 $logger->debug("Creating new copy_transit in DB");
1645 my $s = $session->request(
1646 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1647 return $U->DB_UPDATE_FAILED($transit) unless $s;
1649 $logger->info("Checkin copy successfully created new transit: $s");
1651 $copy->status($U->copy_status_from_name('in transit')->id );
1653 return $U->update_copy( copy => $copy,
1654 editor => $requestor->id, session => $session );
1659 # returns event on error, undef on success
1660 sub _checkin_build_hold_transit {
1663 my $copy = $ctx->{copy};
1664 my $hold = $ctx->{hold};
1665 my $trans = Fieldmapper::action::hold_transit_copy->new;
1667 $trans->hold($hold->id);
1668 $trans->source($ctx->{requestor}->ws_ou);
1669 $trans->dest($hold->pickup_lib);
1670 $trans->source_send_time("now");
1671 $trans->target_copy($copy->id);
1672 $trans->copy_status($copy->status);
1674 my $id = $ctx->{session}->request(
1675 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1676 return $U->DB_UPDATE_FAILED($trans) unless $id;
1678 $logger->info("Checkin copy successfully created hold transit: $id");
1680 $copy->status($U->copy_status_from_name('in transit')->id );
1681 return $U->update_copy( copy => $copy,
1682 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1685 # Returns event on error, undef on success
1686 sub _checkin_capture_hold {
1688 my $copy = $ctx->{copy};
1689 my $hold = $ctx->{hold};
1691 $logger->debug("Checkin copy capturing hold ".$hold->id);
1693 $hold->current_copy($copy->id);
1694 $hold->capture_time('now');
1696 my $stat = $ctx->{session}->request(
1697 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1698 return $U->DB_UPDATE_FAILED($hold) unless $stat;
1700 $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1702 return $U->update_copy( copy => $copy,
1703 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1706 # fleshes an event with the relevant objects from the context
1707 sub _checkin_flesh_event {
1712 $payload->{copy} = $U->unflesh_copy($ctx->{copy});
1713 $payload->{record} = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1714 $payload->{circ} = $ctx->{circ} if $ctx->{circ};
1715 $payload->{transit} = $ctx->{transit} if $ctx->{transit};
1716 $payload->{hold} = $ctx->{hold} if $ctx->{hold};
1718 $evt->{payload} = $payload;
1723 # Closes out the circulation, puts the copy into reshelving.
1724 # Voids any bills attached to this circ after the backdate time
1725 # if a backdate is provided
1726 sub _checkin_handle_circ {
1730 my $circ = $ctx->{circ};
1731 my $copy = $ctx->{copy};
1732 my $requestor = $ctx->{requestor};
1733 my $session = $ctx->{session};
1737 $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1739 # backdate the circ if necessary
1740 if(my $backdate = $ctx->{backdate}) {
1741 return $evt if ($evt =
1742 _checkin_handle_backdate($backdate, $circ, $requestor, $session, 1));
1746 if(!$circ->stop_fines) {
1747 $circ->stop_fines('CHECKIN');
1748 $circ->stop_fines('RENEW') if $__isrenewal;
1749 $circ->stop_fines_time('now');
1752 # see if there are any fines owed on this circ. if not, close it
1753 ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1754 return $evt if $evt;
1755 $circ->xact_finish('now') if( $obt->balance_owed != 0 );
1757 # Set the checkin vars since we have the item
1758 $circ->checkin_time('now');
1759 $circ->checkin_staff($requestor->id);
1760 $circ->checkin_lib($requestor->ws_ou);
1762 $evt = _set_copy_reshelving($copy, $requestor->id, $ctx->{session});
1763 return $evt if $evt;
1765 # $copy->status($U->copy_status_from_name('reshelving')->id);
1766 # $evt = $U->update_copy( session => $session,
1767 # copy => $copy, editor => $requestor->id );
1768 # return $evt if $evt;
1770 $ctx->{session}->request(
1771 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1776 sub _set_copy_reshelving {
1777 my( $copy, $reqr, $session ) = @_;
1779 $logger->info("Setting copy ".$copy->id." to reshelving");
1780 $copy->status($U->copy_status_from_name('reshelving')->id);
1782 my $evt = $U->update_copy(
1783 session => $session,
1787 return $evt if $evt;
1790 # returns event on error, undef on success
1791 # This voids all bills attached to the given circulation that occurred
1792 # after the backdate
1793 # THIS DOES NOT CLOSE THE CIRC if there are no more fines on the item
1794 sub _checkin_handle_backdate {
1795 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1797 $logger->activity("User ".$requestor->id.
1798 " backdating circ [".$circ->target_copy."] to date: $backdate");
1800 my $bills = $session->request( # XXX Verify this call is correct
1801 "open-ils.storage.direct.money.billing.search_where.atomic",
1802 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1805 for my $bill (@$bills) {
1807 my $s = $session->request(
1808 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1809 return $U->DB_UPDATE_FAILED($bill) unless $s;
1813 # if the caller elects to attempt to close the circulation
1814 # transaction, then it will be closed if there are not further
1815 # charges on the transaction
1817 #my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1818 #return $evt if $evt;
1819 #$circ->xact_finish($backdate) if $obt->balance_owed <= 0;
1826 sub _find_patron_from_params {
1834 if(my $barcode = $params->{barcode}) {
1835 $logger->debug("circ finding user from params with barcode $barcode");
1836 ($copy, $evt) = $U->fetch_copy_by_barcode($barcode);
1837 return (undef, undef, $evt) if $evt;
1838 ($circ, $evt) = $U->fetch_open_circulation($copy->id);
1839 return (undef, undef, $evt) if $evt;
1840 ($patron, $evt) = $U->fetch_user($circ->usr);
1841 return (undef, undef, $evt) if $evt;
1843 return ($patron, $copy);
1847 # ------------------------------------------------------------------------------
1849 __PACKAGE__->register_method(
1851 api_name => "open-ils.circ.renew.override",
1852 signature => q/@see open-ils.circ.renew/,
1856 __PACKAGE__->register_method(
1858 api_name => "open-ils.circ.renew",
1859 notes => <<" NOTES");
1860 PARAMS( authtoken, circ => circ_id );
1861 open-ils.circ.renew(login_session, circ_object);
1862 Renews the provided circulation. login_session is the requestor of the
1863 renewal and if the logged in user is not the same as circ->usr, then
1864 the logged in user must have RENEW_CIRC permissions.
1868 my( $self, $client, $authtoken, $params ) = @_;
1871 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1874 $params->{override} = 1 if $self->api_name =~ /override/o;
1876 # fetch the patron object one way or another
1877 if( $params->{patron} ) {
1878 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1879 if($evt) { $__isrenewal = 0; return $evt; }
1881 } elsif( $params->{patron_barcode} ) {
1882 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1883 if($evt) { $__isrenewal = 0; return $evt; }
1886 ($patron, $copy, $evt) = _find_patron_from_params($params);
1887 return $evt if $evt;
1888 $params->{copy} = $copy;
1891 # verify our login session
1892 ($requestor, $evt) = $U->checkses($authtoken);
1893 if($evt) { $__isrenewal = 0; return $evt; }
1895 # make sure we have permission to perform a renewal
1896 if( $requestor->id ne $patron->id ) {
1897 $evt = $U->check_perms($requestor->id, $requestor->ws_ou, 'RENEW_CIRC');
1898 if($evt) { $__isrenewal = 0; return $evt; }
1902 # fetch and build the circulation environment
1903 ( $ctx, $evt ) = create_circ_ctx( %$params,
1905 requestor => $requestor,
1908 #fetch_patron_circ_summary => 1,
1909 fetch_copy_statuses => 1,
1910 fetch_copy_locations => 1,
1912 if($evt) { $__isrenewal = 0; return $evt; }
1913 $params->{_ctx} = $ctx;
1915 # make sure they have some renewals left and make sure the circulation exists
1916 ($circ, $evt) = _check_renewal_remaining($ctx);
1917 if($evt) { $__isrenewal = 0; return $evt; }
1918 $ctx->{old_circ} = $circ;
1919 my $renewals = $circ->renewal_remaining - 1;
1921 # run the renew permit script
1922 $evt = _run_renew_scripts($ctx);
1923 if($evt) { $__isrenewal = 0; return $evt; }
1926 #$ctx->{patron} = $ctx->{patron}->id;
1927 $evt = $self->generic_receive($client, $authtoken, $ctx );
1928 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1930 if( !$U->event_equals($evt, 'SUCCESS') ) {
1931 $__isrenewal = 0; return $evt;
1934 # re-fetch the context since objects have changed in the checkin
1935 ( $ctx, $evt ) = create_circ_ctx( %$params,
1937 requestor => $requestor,
1940 #fetch_patron_circ_summary => 1,
1941 fetch_copy_statuses => 1,
1942 fetch_copy_locations => 1,
1944 if($evt) { $__isrenewal = 0; return $evt; }
1945 $params->{_ctx} = $ctx;
1946 $ctx->{renewal_remaining} = $renewals;
1948 # run the circ permit scripts
1949 if( $ctx->{permit_override} ) {
1950 $evt = $U->check_perms(
1951 $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1952 if($evt) { $__isrenewal = 0; return $evt; }
1955 $evt = $self->permit_circ( $client, $authtoken, $params );
1956 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1957 #$ctx->{precat} = 1;
1958 $params->{precat} = 1;
1961 if(!$U->event_equals($evt, 'SUCCESS')) {
1962 if($evt) { $__isrenewal = 0; return $evt; }
1965 $params->{permit_key} = $evt->{payload};
1969 # checkout the item again
1970 $params->{patron} = $ctx->{patron}->id;
1971 $evt = $self->checkout($client, $authtoken, $params );
1973 $logger->activity("user ".$requestor->id." renewl of item ".
1974 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1980 sub _check_renewal_remaining {
1983 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1984 return (undef, $evt) if $evt;
1985 $evt = OpenILS::Event->new(
1986 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1987 return ($circ, $evt);
1990 sub _run_renew_scripts {
1992 my $runner = $ctx->{runner};
1995 $runner->load($scripts{circ_permit_renew});
1996 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1998 my $events = $runner->retrieve('result.events');
1999 $events = [ split(/,/, $events) ];
2000 $logger->activity("circ_permit_renew for user ".
2001 $ctx->{patron}->id." returned events: @$events") if @$events;
2004 push( @allevents, OpenILS::Event->new($_)) for @$events;
2005 return \@allevents if @allevents;