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 );
336 $ctx->{runner} = $runner;
341 sub _add_script_runner_methods {
344 my $runner = $ctx->{runner};
348 # allows a script to fetch a hold that is currently targeting the
350 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
352 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
353 $hold = undef unless $hold;
354 $runner->insert( $key, $hold, 1 );
360 # ------------------------------------------------------------------------------
362 __PACKAGE__->register_method(
363 method => "permit_circ",
364 api_name => "open-ils.circ.checkout.permit",
366 Determines if the given checkout can occur
367 @param authtoken The login session key
368 @param params A trailing hash of named params including
369 barcode : The copy barcode,
370 patron : The patron the checkout is occurring for,
371 renew : true or false - whether or not this is a renewal
372 @return The event that occurred during the permit check.
375 __PACKAGE__->register_method (
376 method => 'permit_circ',
377 api_name => 'open-ils.circ.checkout.permit.override',
378 signature => q/@see open-ils.circ.checkout.permit/,
382 my( $self, $client, $authtoken, $params ) = @_;
385 my $override = $params->{override} = 1 if $self->api_name =~ /override/o;
387 my ( $requestor, $patron, $ctx, $evt, $circ );
389 # check permisson of the requestor
390 ( $requestor, $patron, $evt ) =
391 $U->checkses_requestor(
392 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
396 # fetch and build the circulation environment
397 if( !( $ctx = $params->{_ctx}) ) {
399 ( $ctx, $evt ) = create_circ_ctx( %$params,
401 requestor => $requestor,
403 #fetch_patron_circ_summary => 1,
404 fetch_copy_statuses => 1,
405 fetch_copy_locations => 1,
410 my $copy = $ctx->{copy};
412 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
413 return OpenILS::Event->new('COPY_IN_TRANSIT')
414 if $stat == $U->copy_status_from_name('in transit')->id;
417 $ctx->{authtoken} = $authtoken;
420 if( $ctx->{copy} and ($evt = _handle_claims_returned($ctx)) ) {
421 return $evt unless $U->event_equals($evt, 'SUCCESS');
429 # no claims returned circ was found, check if there is any open circ
430 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
431 ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
432 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
437 $ctx->{permit_key} = _cache_permit_key();
438 my $events = _run_permit_scripts($ctx);
441 $evt = override_events($requestor, $requestor->ws_ou,
442 $events, $authtoken, $ctx->{copy}->id, $client);
444 return OpenILS::Event->new('SUCCESS', payload => $ctx->{permit_key} );
450 sub override_events {
452 my( $requestor, $org, $events, $authtoken, $copyid, $conn ) = @_;
453 $events = [ $events ] unless ref($events) eq 'ARRAY';
456 for my $e (@$events) {
457 my $tc = $e->{textcode};
458 next if $tc eq 'SUCCESS';
459 my $ov = "$tc.override";
460 $logger->info("attempting to override event $ov");
461 my $evt = $U->check_perms( $requestor->id, $org, $ov );
469 __PACKAGE__->register_method(
470 method => "check_title_hold",
471 api_name => "open-ils.circ.title_hold.is_possible",
473 Determines if a hold were to be placed by a given user,
474 whether or not said hold would have any potential copies
476 @param authtoken The login session key
477 @param params A hash of named params including:
478 patronid - the id of the hold recipient
479 titleid (brn) - the id of the title to be held
480 depth - the hold range depth (defaults to 0)
483 # XXX add pickup lib to the call to test for perms
485 sub check_title_hold {
486 my( $self, $client, $authtoken, $params ) = @_;
487 my %params = %$params;
488 my $titleid = $params{titleid};
490 my ( $requestor, $patron, $evt ) = $U->checkses_requestor(
491 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
494 my $rangelib = $patron->home_ou;
495 my $depth = $params{depth} || 0;
496 my $pickup = $params{pickup_lib};
498 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
500 my $org = $U->simplereq(
502 'open-ils.actor.org_unit.retrieve',
503 $authtoken, $requestor->home_ou );
509 while( $title = $U->storagereq(
510 'open-ils.storage.biblio.record_entry.ranged_tree',
511 $titleid, $rangelib, $depth, $limit, $offset ) ) {
515 ref($title->call_numbers) and
516 @{$title->call_numbers};
518 for my $cn (@{$title->call_numbers}) {
520 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
522 for my $copy (@{$cn->copies}) {
524 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
526 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
528 requestor => $requestor,
531 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
532 pickup_lib => $pickup,
533 request_lib => $org } );
535 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
546 # Runs the patron and copy permit scripts
547 # if this is a non-cat circulation, the copy permit script
549 sub _run_permit_scripts {
552 my $runner = $ctx->{runner};
553 my $patronid = $ctx->{patron}->id;
554 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
555 my $key = $ctx->{permit_key};
559 # ---------------------------------------------------------------------
560 # Find all of the fatal penalties currently set on the user
561 # ---------------------------------------------------------------------
562 my $penalties = $U->update_patron_penalties(
563 authtoken => $ctx->{authtoken},
564 patron => $ctx->{patron}
567 $penalties = $penalties->{fatal_penalties};
568 $logger->info("circ patron penalties user $patronid: @$penalties");
571 # ---------------------------------------------------------------------
572 # Now run the patron permit script
573 # ---------------------------------------------------------------------
574 $runner->load($scripts{circ_permit_patron});
575 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
577 my $patron_events = $runner->retrieve('result.events');
578 $patron_events = [ split(/,/, $patron_events) ];
579 $ctx->{circ_permit_patron_events} = $patron_events;
580 $logger->activity("circ_permit_patron for returned @$patron_events") if @$patron_events;
582 my @evts_so_far = (@$penalties, @$patron_events);
584 return \@evts_so_far if @evts_so_far;
587 if( $ctx->{noncat} ) {
588 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
589 return OpenILS::Event->new('SUCCESS', payload => $key);
593 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
594 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
598 $logger->debug("Exiting circ permit early because request is for hold patron permit");
599 return OpenILS::Event->new('SUCCESS');
604 # ---------------------------------------------------------------------
605 # Capture all of the copy permit events
606 # ---------------------------------------------------------------------
607 $runner->load($scripts{circ_permit_copy});
608 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
610 my $copy_events = $runner->retrieve('result.events');
611 $copy_events = [ split(/,/, $copy_events) ];
612 $ctx->{circ_permit_copy_events} = $copy_events;
613 $logger->activity("circ_permit_copy for copy ".
614 "$barcode returned events: @$copy_events") if @$copy_events;
619 # ---------------------------------------------------------------------
620 # Now collect all of the events together
621 # ---------------------------------------------------------------------
623 my @allevents; #= ( @evts_so_far, @$copy_events );
624 push( @allevents, OpenILS::Event->new($_)) for @evts_so_far;
625 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
627 my $ae = _check_copy_alert($ctx->{copy});
628 push( @allevents, $ae ) if $ae;
630 return OpenILS::Event->new('SUCCESS', payload => $key) unless (@allevents);
633 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
634 @allevents = values %hash;
637 $_->{payload} = $ctx->{copy}->status->id
638 if ($_->{textcode} eq 'COPY_NOT_AVAILABLE');
644 sub _check_copy_alert {
646 return OpenILS::Event->new('COPY_ALERT_MESSAGE',
647 payload => $copy->alert_message) if $copy->alert_message;
651 # takes copyid, patronid, and requestor id
652 sub _cache_permit_key {
653 my $key = md5_hex( time() . rand() . "$$" );
654 $logger->debug("Setting circ permit key to $key");
655 $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
659 sub _check_permit_key {
661 $logger->debug("Fetching circ permit key $key");
662 my $k = "oils_permit_key_$key";
663 my $one = $cache_handle->get_cache($k);
664 $cache_handle->delete_cache($k);
665 return ($one) ? 1 : 0;
669 # ------------------------------------------------------------------------------
671 __PACKAGE__->register_method(
672 method => "checkout",
673 api_name => "open-ils.circ.checkout",
676 @param authtoken The login session key
677 @param params A named hash of params including:
679 barcode If no copy is provided, the copy is retrieved via barcode
680 copyid If no copy or barcode is provide, the copy id will be use
681 patron The patron's id
682 noncat True if this is a circulation for a non-cataloted item
683 noncat_type The non-cataloged type id
684 noncat_circ_lib The location for the noncat circ.
685 precat The item has yet to be cataloged
686 dummy_title The temporary title of the pre-cataloded item
687 dummy_author The temporary authr of the pre-cataloded item
688 Default is the home org of the staff member
689 @return The SUCCESS event on success, any other event depending on the error
693 my( $self, $client, $authtoken, $params ) = @_;
696 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
697 my $key = $params->{permit_key};
699 # if this is a renewal, then the requestor does not have to
700 # have checkout privelages
701 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
702 ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
705 if( $params->{patron} ) {
706 ( $patron, $evt ) = $U->fetch_user($params->{patron});
709 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
713 # set the circ lib to the home org of the requestor if not specified
714 my $circlib = (defined($params->{circ_lib})) ?
715 $params->{circ_lib} : $requestor->ws_ou;
718 # Make sure the caller has a valid permit key or is
719 # overriding the permit can
720 if( $params->{permit_override} ) {
721 $evt = $U->check_perms(
722 $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
726 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
727 unless _check_permit_key($key);
730 # if this is a non-cataloged item, check it out and return
731 return _checkout_noncat(
732 $key, $requestor, $patron, %$params ) if $params->{noncat};
734 # if this item has yet to be cataloged, make sure a dummy copy exists
735 ( $params->{copy}, $evt ) = _make_precat_copy(
736 $requestor, $circlib, $params ) if $params->{precat};
740 # fetch and build the circulation environment
741 if( !( $ctx = $params->{_ctx}) ) {
742 ( $ctx, $evt ) = create_circ_ctx( %$params,
744 requestor => $requestor,
745 session => $U->start_db_session(),
747 #fetch_patron_circ_summary => 1,
748 fetch_copy_statuses => 1,
749 fetch_copy_locations => 1,
753 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
755 # if the call doesn't know it's not cataloged..
756 if(!$params->{precat}) {
757 if( $ctx->{copy}->call_number eq '-1' ) {
758 return OpenILS::Event->new('ITEM_NOT_CATALOGED');
763 $copy = $ctx->{copy};
765 my $stat = (ref $copy->status) ? $copy->status->id : $copy->status;
766 return OpenILS::Event->new('COPY_IN_TRANSIT')
767 if $stat == $U->copy_status_from_name('in transit')->id;
770 # this happens in permit.. but we need to check here for 'offline' requests
771 ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
772 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
774 my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
777 $ctx->{circ_lib} = $circlib;
779 $evt = _run_checkout_scripts($ctx);
783 _build_checkout_circ_object($ctx);
785 $evt = _apply_modified_due_date($ctx);
788 $evt = _commit_checkout_circ_object($ctx);
791 $evt = _update_checkout_copy($ctx);
795 ($holds, $evt) = _handle_related_holds($ctx);
799 $logger->debug("Checkout committing objects with session thread trace: ".$ctx->{session}->session_id);
800 $U->commit_db_session($ctx->{session});
801 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
803 $logger->activity("user ".$requestor->id." successfully checked out item ".
804 $ctx->{copy}->barcode." to user ".$ctx->{patron}->id );
807 # ------------------------------------------------------------------------------
808 # Update the patron penalty info in the DB
809 # ------------------------------------------------------------------------------
810 $U->update_patron_penalties(
811 authtoken => $authtoken,
812 patron => $ctx->{patron} ,
816 return OpenILS::Event->new('SUCCESS',
818 copy => $U->unflesh_copy($ctx->{copy}),
819 circ => $ctx->{circ},
821 holds_fulfilled => $holds,
827 sub _make_precat_copy {
828 my ( $requestor, $circlib, $params ) = @_;
830 my( $copy, undef ) = _find_copy_by_attr(%$params);
833 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
835 $copy->editor($requestor->id);
836 $copy->edit_date('now');
837 $copy->dummy_title($params->{dummy_title});
838 $copy->dummy_author($params->{dummy_author});
840 my $stat = $U->storagereq(
841 'open-ils.storage.direct.asset.copy.update', $copy );
843 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $stat;
847 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
849 my $evt = OpenILS::Event->new(
850 'BAD_PARAMS', desc => "Dummy title or author not provided" )
851 unless ( $params->{dummy_title} and $params->{dummy_author} );
852 return (undef, $evt) if $evt;
854 $copy = Fieldmapper::asset::copy->new;
855 $copy->circ_lib($circlib);
856 $copy->creator($requestor->id);
857 $copy->editor($requestor->id);
858 $copy->barcode($params->{barcode});
859 $copy->call_number(-1); #special CN for precat materials
860 $copy->loan_duration(&PRECAT_LOAN_DURATION);
861 $copy->fine_level(&PRECAT_FINE_LEVEL);
863 $copy->dummy_title($params->{dummy_title});
864 $copy->dummy_author($params->{dummy_author});
866 my $id = $U->storagereq(
867 'open-ils.storage.direct.asset.copy.create', $copy );
868 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
870 $logger->debug("Pre-cataloged copy successfully created");
871 return ($U->fetch_copy($id));
875 sub _run_checkout_scripts {
881 my $runner = $ctx->{runner};
883 $runner->insert('result.durationLevel');
884 $runner->insert('result.durationRule');
885 $runner->insert('result.recurringFinesRule');
886 $runner->insert('result.recurringFinesLevel');
887 $runner->insert('result.maxFine');
889 $runner->load($scripts{circ_duration});
890 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
891 my $duration = $runner->retrieve('result.durationRule');
892 $logger->debug("Circ duration script yielded a duration rule of: $duration");
894 $runner->load($scripts{circ_recurring_fines});
895 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
896 my $recurring = $runner->retrieve('result.recurringFinesRule');
897 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
899 $runner->load($scripts{circ_max_fines});
900 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
901 my $max_fine = $runner->retrieve('result.maxFine');
902 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
904 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
906 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
908 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
911 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
912 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
913 $ctx->{duration_rule} = $duration;
914 $ctx->{recurring_fines_rule} = $recurring;
915 $ctx->{max_fine_rule} = $max_fine;
920 sub _build_checkout_circ_object {
924 my $circ = new Fieldmapper::action::circulation;
925 my $duration = $ctx->{duration_rule};
926 my $max = $ctx->{max_fine_rule};
927 my $recurring = $ctx->{recurring_fines_rule};
928 my $copy = $ctx->{copy};
929 my $patron = $ctx->{patron};
930 my $dur_level = $ctx->{duration_level};
931 my $rec_level = $ctx->{recurring_fines_level};
933 $circ->duration( $duration->shrt ) if ($dur_level == 1);
934 $circ->duration( $duration->normal ) if ($dur_level == 2);
935 $circ->duration( $duration->extended ) if ($dur_level == 3);
937 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
938 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
939 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
941 $circ->duration_rule( $duration->name );
942 $circ->recuring_fine_rule( $recurring->name );
943 $circ->max_fine_rule( $max->name );
944 $circ->max_fine( $max->amount );
946 $circ->fine_interval($recurring->recurance_interval);
947 $circ->renewal_remaining( $duration->max_renewals );
948 $circ->target_copy( $copy->id );
949 $circ->usr( $patron->id );
950 $circ->circ_lib( $ctx->{circ_lib} );
953 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
954 $circ->opac_renewal(1);
955 $circ->renewal_remaining($ctx->{renewal_remaining});
956 $circ->circ_staff($ctx->{requestor}->id);
960 # if the user provided an overiding checkout time,
961 # (e.g. the checkout really happened several hours ago), then
962 # we apply that here. Does this need a perm??
963 if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
964 $logger->debug("circ setting checkout_time to $ds");
965 $circ->xact_start($ds);
968 # if a patron is renewing, 'requestor' will be the patron
969 $circ->circ_staff($ctx->{requestor}->id );
970 _set_circ_due_date($circ);
971 $ctx->{circ} = $circ;
974 sub _apply_modified_due_date {
976 my $circ = $ctx->{circ};
980 if( $ctx->{due_date} ) {
982 my $evt = $U->check_perms(
983 $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
986 my $ds = _create_date_stamp($ctx->{due_date});
987 $logger->debug("circ modifying due_date to $ds");
988 $circ->due_date($ds);
992 # if the due_date lands on a day when the location is closed
993 my $copy = $ctx->{copy};
996 $logger->info("circ searching for closed date overlap on lib ".
997 $copy->circ_lib->id ." with an item due date of ".$circ->due_date );
999 my $dateinfo = $ctx->{session}->request(
1000 'open-ils.storage.actor.org_unit.closed_date.overlap',
1001 $copy->circ_lib->id, $circ->due_date )->gather(1);
1005 $logger->info("$dateinfo : circ due data / close date overlap found : due_date=".
1006 $circ->due_date." start=". $dateinfo->{start}.", end=".$dateinfo->{end});
1008 # XXX make the behavior more dynamic
1009 # for now, we just push the due date to after the close date
1010 $circ->due_date($dateinfo->{end});
1017 sub _create_date_stamp {
1018 my $datestring = shift;
1019 return undef unless $datestring;
1020 $datestring = clense_ISO8601($datestring);
1021 $logger->debug("circ created date stamp => $datestring");
1025 sub _create_due_date {
1026 my $duration = shift;
1028 my ($sec,$min,$hour,$mday,$mon,$year) =
1029 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
1030 $year += 1900; $mon += 1;
1031 my $due_date = sprintf(
1032 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
1033 $year, $mon, $mday, $hour, $min, $sec);
1037 sub _set_circ_due_date {
1040 my $dd = _create_due_date($circ->duration);
1041 $logger->debug("Checkout setting due date on circ to: $dd");
1042 $circ->due_date($dd);
1045 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
1046 sub _update_checkout_copy {
1049 my $copy = $ctx->{copy};
1051 my $s = $U->copy_status_from_name('checked out');
1052 $copy->status( $s->id ) if $s;
1054 my $evt = $U->update_copy( session => $ctx->{session},
1055 copy => $copy, editor => $ctx->{requestor}->id );
1056 return (undef,$evt) if $evt;
1061 # commits the circ object to the db then fleshes the circ with rules objects
1062 sub _commit_checkout_circ_object {
1065 my $circ = $ctx->{circ};
1069 my $r = $ctx->{session}->request(
1070 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
1072 return $U->DB_UPDATE_FAILED($circ) unless $r;
1074 $logger->debug("Created a new circ object in checkout: $r");
1077 $circ->duration_rule($ctx->{duration_rule});
1078 $circ->max_fine_rule($ctx->{max_fine_rule});
1079 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
1085 # sees if there are any holds that this copy
1086 sub _handle_related_holds {
1089 my $copy = $ctx->{copy};
1090 my $patron = $ctx->{patron};
1091 my $holds = $holdcode->fetch_related_holds($copy->id);
1095 # XXX We should only fulfill one hold here...
1096 # XXX If a hold was transited to the user who is checking out
1097 # the item, we need to make sure that hold is what's grabbed
1098 if(ref($holds) && @$holds) {
1100 # for now, just sort by id to get what should be the oldest hold
1101 $holds = [ sort { $a->id <=> $b->id } @$holds ];
1102 my @myholds = grep { $_->usr eq $patron->id } @$holds;
1103 my @altholds = grep { $_->usr ne $patron->id } @$holds;
1106 my $hold = $myholds[0];
1108 $logger->debug("Related hold found in checkout: " . $hold->id );
1110 $hold->current_copy($copy->id); # just make sure it's set
1111 # if the hold was never officially captured, capture it.
1112 $hold->capture_time('now') unless $hold->capture_time;
1113 $hold->fulfillment_time('now');
1114 my $r = $ctx->{session}->request(
1115 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
1116 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
1117 push( @fulfilled, $hold->id );
1120 # If there are any holds placed for other users that point to this copy,
1121 # then we need to un-target those holds so the targeter can pick a new copy
1124 $logger->info("Un-targeting hold ".$_->id.
1125 " because copy ".$copy->id." is getting checked out");
1127 $_->clear_current_copy;
1128 my $r = $ctx->{session}->request(
1129 "open-ils.storage.direct.action.hold_request.update", $_ )->gather(1);
1130 return (undef,$U->DB_UPDATE_FAILED( $_ )) unless $r;
1134 return (\@fulfilled, undef);
1137 sub _checkout_noncat {
1138 my ( $key, $requestor, $patron, %params ) = @_;
1139 my( $circ, $circlib, $evt );
1142 $circlib = $params{noncat_circ_lib} || $requestor->ws_ou;
1144 my $count = $params{noncat_count} || 1;
1145 my $cotime = _create_date_stamp($params{checkout_time}) || "";
1146 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1148 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1149 $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
1150 return $evt if $evt;
1153 return OpenILS::Event->new(
1154 'SUCCESS', payload => { noncat_circ => $circ } );
1158 __PACKAGE__->register_method(
1159 method => "generic_receive",
1160 api_name => "open-ils.circ.checkin",
1163 Generic super-method for handling all copies
1164 @param authtoken The login session key
1165 @param params Hash of named parameters including:
1166 barcode - The copy barcode
1167 force - If true, copies in bad statuses will be checked in and give good statuses
1172 __PACKAGE__->register_method(
1173 method => "generic_receive",
1174 api_name => "open-ils.circ.checkin.override",
1175 signature => q/@see open-ils.circ.checkin/
1178 sub generic_receive {
1179 my( $self, $conn, $authtoken, $params ) = @_;
1180 my( $ctx, $requestor, $evt );
1182 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1183 ( $requestor, $evt ) = $U->checksesperm(
1184 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1185 return $evt if $evt;
1187 # load up the circ objects
1188 if( !( $ctx = $params->{_ctx}) ) {
1189 ( $ctx, $evt ) = create_circ_ctx( %$params,
1190 requestor => $requestor,
1191 session => $U->start_db_session(),
1193 fetch_copy_statuses => 1,
1194 fetch_copy_locations => 1,
1197 return $evt if $evt;
1199 $ctx->{override} = 1 if $self->api_name =~ /override/o;
1200 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1201 $ctx->{authtoken} = $authtoken;
1202 my $session = $ctx->{session};
1204 my $copy = $ctx->{copy};
1205 $U->unflesh_copy($copy);
1206 return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') unless $copy;
1208 $logger->info("Checkin copy called by user ".
1209 $requestor->id." for copy ".$copy->id);
1212 my $val = $self->checkin_do_receive($conn, $ctx);
1214 # ------------------------------------------------------------------------------
1215 # Update the patron penalty info in the DB
1216 # ------------------------------------------------------------------------------
1217 $U->update_patron_penalties(
1218 authtoken => $authtoken,
1219 patron => $ctx->{patron},
1226 sub checkin_do_receive {
1228 my( $self, $connection, $ctx ) = @_;
1231 my $copy = $ctx->{copy};
1232 my $session = $ctx->{session};
1233 my $requestor = $ctx->{requestor};
1234 my $change = 0; # did we actually do anything?
1239 # does the copy have an attached alert message?
1240 my $ae = _check_copy_alert($copy);
1241 push(@eventlist, $ae) if $ae;
1243 # is the copy is an a status we can't automatically resolve?
1244 $evt = _checkin_check_copy_status($ctx);
1245 push( @eventlist, $evt ) if $evt;
1248 # - see if the copy has an open circ attached
1249 #($ctx->{circ}, $evt) = $U->fetch_open_circulation($copy->id);
1250 ($ctx->{circ}, $evt) = $U->fetch_all_open_circulation($copy->id); # - get ones with stop fines as well
1251 return $evt if ($evt and $__isrenewal); # renewals require a circulation
1253 $circ = $ctx->{circ};
1255 # if the circ is marked as 'claims returned', add the event to the list
1256 push( @eventlist, OpenILS::Event->new('CIRC_CLAIMS_RETURNED') )
1257 if ($circ and $circ->stop_fines and $circ->stop_fines eq 'CLAIMSRETURNED');
1261 if($ctx->{override}) {
1262 $evt = override_events($requestor, $requestor->ws_ou, \@eventlist );
1263 return $evt if $evt;
1269 ($ctx->{transit}) = $U->fetch_open_transit_by_copy($copy->id);
1271 if( $ctx->{circ} ) {
1273 # There is an open circ on this item, close it out.
1275 $evt = _checkin_handle_circ($ctx);
1276 return $evt if $evt;
1278 } elsif( $ctx->{transit} ) {
1280 # is this item currently in transit?
1282 $evt = $transcode->transit_receive( $copy, $requestor, $session );
1283 my $holdtrans = $evt->{holdtransit};
1284 ($ctx->{hold}) = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1286 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1288 # either an error occurred or a ROUTE_ITEM was generated and the
1289 # item must be forwarded on to its destination.
1290 return _checkin_flesh_event($ctx, $evt);
1294 # Transit has been closed, now let's see if the copy's original
1295 # status is something the staff should be warned of
1296 my $e = _checkin_check_copy_status($ctx);
1301 # copy was received as a hold transit. Copy is at target lib
1302 # and hold transit is complete. We're done here...
1303 $U->commit_db_session($session);
1304 return _checkin_flesh_event($ctx, $evt);
1310 # ------------------------------------------------------------------------------
1311 # Circulations and transits are now closed where necessary. Now go on to see if
1312 # this copy can fulfill a hold or needs to be routed to a different location
1313 # ------------------------------------------------------------------------------
1316 # If it's a renewal, we're done
1319 #my ($cc, $ee) = _reshelve_copy($ctx);
1321 #delete $$ctx{force};
1322 $U->commit_db_session($session);
1323 return OpenILS::Event->new('SUCCESS');
1326 # Now, let's see if this copy is needed for a hold
1327 my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor );
1331 $ctx->{hold} = $hold;
1334 # Capture the hold with this copy
1335 return $evt if ($evt = _checkin_capture_hold($ctx));
1337 if( $hold->pickup_lib == $requestor->ws_ou ) {
1339 # This hold was captured in the correct location
1340 $evt = OpenILS::Event->new('SUCCESS');
1344 # Hold needs to be picked up elsewhere. Build a hold
1345 # transit and route the item.
1346 return $evt if ($evt =_checkin_build_hold_transit($ctx));
1347 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1350 } else { # not needed for a hold
1352 if( $copy->circ_lib == $requestor->ws_ou ) {
1354 # Copy is in the right place.
1355 $evt = OpenILS::Event->new('SUCCESS');
1357 # if the item happens to be a pre-cataloged item, send it
1358 # to cataloging and return the event
1359 my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1360 return $err if $err;
1366 # Copy wants to go home. Transit it there.
1367 return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1368 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1374 # ------------------------------------------------------------------
1375 # if the copy is not in a state that should persist,
1376 # set the copy to reshelving if it's not already there
1377 # ------------------------------------------------------------------
1378 my ($c, $e) = _reshelve_copy($ctx);
1380 $change = $c unless $change;
1384 $evt = OpenILS::Event->new('NO_CHANGE');
1385 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id)
1388 if( $copy->status == $U->copy_status_from_name('on holds shelf')->id );
1392 $U->commit_db_session($session);
1395 $logger->activity("checkin by user ".$requestor->id." on item ".
1396 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1398 return _checkin_flesh_event($ctx, $evt);
1401 sub _reshelve_copy {
1404 my $copy = $ctx->{copy};
1405 my $reqr = $ctx->{requestor};
1406 my $session = $ctx->{session};
1407 my $force = $ctx->{force};
1409 my $stat = ref($copy->status) ? $copy->status->id : $copy->status;
1412 $stat != $U->copy_status_from_name('on holds shelf')->id and
1413 $stat != $U->copy_status_from_name('available')->id and
1414 $stat != $U->copy_status_from_name('cataloging')->id and
1415 $stat != $U->copy_status_from_name('in transit')->id and
1416 $stat != $U->copy_status_from_name('reshelving')->id) ) {
1418 $copy->status( $U->copy_status_from_name('reshelving')->id );
1420 my $evt = $U->update_copy(
1422 editor => $reqr->id,
1423 session => $session,
1434 # returns undef if there are no 'open' claims-returned circs attached
1435 # to the given copy. if there is an open claims-returned circ,
1436 # then we check for override mode. if in override, mark the claims-returned
1437 # circ as checked in. if not, return event.
1438 sub _handle_claims_returned {
1440 my $copy = $ctx->{copy};
1442 my $CR = _fetch_open_claims_returned($copy->id);
1443 return undef unless $CR;
1445 # - If the caller has set the override flag, we will check the item in
1446 if($ctx->{override}) {
1448 $CR->checkin_time('now');
1449 $CR->checkin_lib($ctx->{requestor}->ws_ou);
1450 $CR->checkin_staff($ctx->{requestor}->id);
1452 my $stat = $U->storagereq(
1453 'open-ils.storage.direct.action.circulation.update', $CR);
1454 return $U->DB_UPDATE_FAILED($CR) unless $stat;
1455 return OpenILS::Event->new('SUCCESS');
1458 # - if not in override mode, return the CR event
1459 return OpenILS::Event->new('CIRC_CLAIMS_RETURNED');
1464 sub _fetch_open_claims_returned {
1466 my $trans = $U->storagereq(
1467 'open-ils.storage.direct.action.circulation.search_where',
1469 target_copy => $copyid,
1470 stop_fines => 'CLAIMSRETURNED',
1471 checkin_time => undef,
1474 return $$trans[0] if $trans && $$trans[0];
1478 # - if the copy is has the 'in process' status, set it to reshelving
1479 #sub _check_in_process {
1482 #my $copy = $ctx->{copy};
1483 #my $reqr = $ctx->{requestor};
1484 #my $ses = $ctx->{session};
1486 #my $stat = $U->copy_status_from_name('in process');
1487 #my $rstat = $U->copy_status_from_name('reshelving');
1489 #if( $stat->id == $copy->status->id ) {
1490 #$logger->info("marking 'in-process' copy ".$copy->id." as 'reshelving'");
1491 #$copy->status( $rstat->id );
1492 #my $evt = $U->update_copy(
1494 #editor => $reqr->id,
1497 #return $evt if $evt;
1499 #$copy->status( $rstat ); # - reflesh the copy status
1505 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1506 sub _checkin_handle_precat {
1509 my $copy = $ctx->{copy};
1514 my $catstat = $U->copy_status_from_name('cataloging');
1516 if( $ctx->{precat} ) {
1518 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1520 if( $copy->status != $catstat->id ) {
1521 $copy->status($catstat->id);
1523 return (undef, 0, $errevt) if (
1524 $errevt = $U->update_copy(
1526 editor => $ctx->{requestor}->id,
1527 session => $ctx->{session} ));
1533 return ($evt, $change, undef);
1537 # returns the appropriate event for the given copy status
1538 # if the copy is not in a 'special' status, undef is returned
1539 sub _checkin_check_copy_status {
1541 my $copy = $ctx->{copy};
1542 my $reqr = $ctx->{requestor};
1543 my $ses = $ctx->{session};
1549 my $status = ref($copy->status) ? $copy->status->id : $copy->status;
1552 if( $status == $U->copy_status_from_name('available')->id ||
1553 $status == $U->copy_status_from_name('checked out')->id ||
1554 $status == $U->copy_status_from_name('in process')->id ||
1555 $status == $U->copy_status_from_name('in transit')->id ||
1556 $status == $U->copy_status_from_name('reshelving')->id );
1558 return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
1559 if( $status == $U->copy_status_from_name('lost')->id );
1561 return OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy )
1562 if( $status == $U->copy_status_from_name('missing')->id );
1564 return OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1568 # my $rstat = $U->copy_status_from_name('reshelving');
1569 # my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1571 # if( $stat == $U->copy_status_from_name('lost')->id ) {
1573 # $evt = OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy );
1575 # } elsif( $stat == $U->copy_status_from_name('missing')->id) {
1577 # $evt = OpenILS::Event->new('COPY_STATUS_MISSING', payload => $copy );
1580 # return (undef,$evt) if(!$ctx->{override});
1582 # # we're are now going to attempt to override the failure
1583 # # and set the copy to reshelving
1585 # my $copyid = $copy->id;
1586 # my $userid = $reqr->id;
1589 # # - make sure we have permission
1590 # $e = $U->check_perms( $reqr->id,
1591 # $copy->circ_lib, 'COPY_STATUS_LOST.override');
1592 # return (undef,$e) if $e;
1593 # $copy->status( $rstat->id );
1595 # # XXX if no fines are owed in the circ, close it out - will this happen later anyway?
1596 # #my $circ = $U->storagereq(
1597 # # 'open-ils.storage.direct.action.circulation
1599 # $logger->activity("user $userid overriding 'lost' copy status for copy $copyid");
1601 # } elsif( $ismissing ) {
1603 # # - make sure we have permission
1604 # $e = $U->check_perms( $reqr->id,
1605 # $copy->circ_lib, 'COPY_STATUS_MISSING.override');
1606 # return (undef,$e) if $e;
1607 # $copy->status( $rstat->id );
1608 # $logger->activity("user $userid overriding 'missing' copy status for copy $copyid");
1611 # if( $islost or $ismissing ) {
1613 # # - update the copy with the new status
1614 # $evt = $U->update_copy(
1616 # editor => $reqr->id,
1619 # return (undef,$evt) if $evt;
1620 # $copy->status( $rstat );
1628 # Just gets the copy back home. Returns undef on success, event on error
1629 sub _checkin_build_generic_copy_transit {
1632 my $requestor = $ctx->{requestor};
1633 my $copy = $ctx->{copy};
1634 my $transit = Fieldmapper::action::transit_copy->new;
1635 my $session = $ctx->{session};
1637 $logger->activity("User ". $requestor->id ." creating a ".
1638 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1640 $transit->source($requestor->ws_ou);
1641 $transit->dest($copy->circ_lib);
1642 $transit->target_copy($copy->id);
1643 $transit->source_send_time('now');
1644 $transit->copy_status($copy->status);
1646 $logger->debug("Creating new copy_transit in DB");
1648 my $s = $session->request(
1649 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1650 return $U->DB_UPDATE_FAILED($transit) unless $s;
1652 $logger->info("Checkin copy successfully created new transit: $s");
1654 $copy->status($U->copy_status_from_name('in transit')->id );
1656 return $U->update_copy( copy => $copy,
1657 editor => $requestor->id, session => $session );
1662 # returns event on error, undef on success
1663 sub _checkin_build_hold_transit {
1666 my $copy = $ctx->{copy};
1667 my $hold = $ctx->{hold};
1668 my $trans = Fieldmapper::action::hold_transit_copy->new;
1670 $trans->hold($hold->id);
1671 $trans->source($ctx->{requestor}->ws_ou);
1672 $trans->dest($hold->pickup_lib);
1673 $trans->source_send_time("now");
1674 $trans->target_copy($copy->id);
1675 $trans->copy_status($copy->status);
1677 my $id = $ctx->{session}->request(
1678 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1679 return $U->DB_UPDATE_FAILED($trans) unless $id;
1681 $logger->info("Checkin copy successfully created hold transit: $id");
1683 $copy->status($U->copy_status_from_name('in transit')->id );
1684 return $U->update_copy( copy => $copy,
1685 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1688 # Returns event on error, undef on success
1689 sub _checkin_capture_hold {
1691 my $copy = $ctx->{copy};
1692 my $hold = $ctx->{hold};
1694 $logger->debug("Checkin copy capturing hold ".$hold->id);
1696 $hold->current_copy($copy->id);
1697 $hold->capture_time('now');
1699 my $stat = $ctx->{session}->request(
1700 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1701 return $U->DB_UPDATE_FAILED($hold) unless $stat;
1703 $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1705 return $U->update_copy( copy => $copy,
1706 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1709 # fleshes an event with the relevant objects from the context
1710 sub _checkin_flesh_event {
1715 $payload->{copy} = $U->unflesh_copy($ctx->{copy});
1716 $payload->{record} = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1717 $payload->{circ} = $ctx->{circ} if $ctx->{circ};
1718 $payload->{transit} = $ctx->{transit} if $ctx->{transit};
1719 $payload->{hold} = $ctx->{hold} if $ctx->{hold};
1721 $evt->{payload} = $payload;
1726 # Closes out the circulation, puts the copy into reshelving.
1727 # Voids any bills attached to this circ after the backdate time
1728 # if a backdate is provided
1729 sub _checkin_handle_circ {
1733 my $circ = $ctx->{circ};
1734 my $copy = $ctx->{copy};
1735 my $requestor = $ctx->{requestor};
1736 my $session = $ctx->{session};
1740 $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1742 # backdate the circ if necessary
1743 if(my $backdate = $ctx->{backdate}) {
1744 return $evt if ($evt =
1745 _checkin_handle_backdate($backdate, $circ, $requestor, $session, 1));
1749 if(!$circ->stop_fines) {
1750 $circ->stop_fines('CHECKIN');
1751 $circ->stop_fines('RENEW') if $__isrenewal;
1752 $circ->stop_fines_time('now');
1755 # see if there are any fines owed on this circ. if not, close it
1756 ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1757 return $evt if $evt;
1758 $circ->xact_finish('now') if( $obt->balance_owed == 0 );
1760 # Set the checkin vars since we have the item
1761 $circ->checkin_time('now');
1762 $circ->checkin_staff($requestor->id);
1763 $circ->checkin_lib($requestor->ws_ou);
1765 $evt = _set_copy_reshelving($copy, $requestor->id, $ctx->{session});
1766 return $evt if $evt;
1768 # $copy->status($U->copy_status_from_name('reshelving')->id);
1769 # $evt = $U->update_copy( session => $session,
1770 # copy => $copy, editor => $requestor->id );
1771 # return $evt if $evt;
1773 $ctx->{session}->request(
1774 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1779 sub _set_copy_reshelving {
1780 my( $copy, $reqr, $session ) = @_;
1782 $logger->info("Setting copy ".$copy->id." to reshelving");
1783 $copy->status($U->copy_status_from_name('reshelving')->id);
1785 my $evt = $U->update_copy(
1786 session => $session,
1790 return $evt if $evt;
1793 # returns event on error, undef on success
1794 # This voids all bills attached to the given circulation that occurred
1795 # after the backdate
1796 # THIS DOES NOT CLOSE THE CIRC if there are no more fines on the item
1797 sub _checkin_handle_backdate {
1798 my( $backdate, $circ, $requestor, $session, $closecirc ) = @_;
1800 $logger->activity("User ".$requestor->id.
1801 " backdating circ [".$circ->target_copy."] to date: $backdate");
1803 my $bills = $session->request( # XXX Verify this call is correct
1804 "open-ils.storage.direct.money.billing.search_where.atomic",
1805 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1808 for my $bill (@$bills) {
1810 my $n = $bill->note || "";
1811 $bill->note($n . "\nSYSTEM VOIDED FOR BACKDATE");
1812 my $s = $session->request(
1813 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1814 return $U->DB_UPDATE_FAILED($bill) unless $s;
1818 # if the caller elects to attempt to close the circulation
1819 # transaction, then it will be closed if there are not further
1820 # charges on the transaction
1822 #my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1823 #return $evt if $evt;
1824 #$circ->xact_finish($backdate) if $obt->balance_owed <= 0;
1831 sub _find_patron_from_params {
1839 if(my $barcode = $params->{barcode}) {
1840 $logger->debug("circ finding user from params with barcode $barcode");
1841 ($copy, $evt) = $U->fetch_copy_by_barcode($barcode);
1842 return (undef, undef, $evt) if $evt;
1843 ($circ, $evt) = $U->fetch_open_circulation($copy->id);
1844 return (undef, undef, $evt) if $evt;
1845 ($patron, $evt) = $U->fetch_user($circ->usr);
1846 return (undef, undef, $evt) if $evt;
1848 return ($patron, $copy);
1852 # ------------------------------------------------------------------------------
1854 __PACKAGE__->register_method(
1856 api_name => "open-ils.circ.renew.override",
1857 signature => q/@see open-ils.circ.renew/,
1861 __PACKAGE__->register_method(
1863 api_name => "open-ils.circ.renew",
1864 notes => <<" NOTES");
1865 PARAMS( authtoken, circ => circ_id );
1866 open-ils.circ.renew(login_session, circ_object);
1867 Renews the provided circulation. login_session is the requestor of the
1868 renewal and if the logged in user is not the same as circ->usr, then
1869 the logged in user must have RENEW_CIRC permissions.
1873 my( $self, $client, $authtoken, $params ) = @_;
1876 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1879 $params->{override} = 1 if $self->api_name =~ /override/o;
1881 # fetch the patron object one way or another
1882 if( $params->{patron} ) {
1883 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1884 if($evt) { $__isrenewal = 0; return $evt; }
1886 } elsif( $params->{patron_barcode} ) {
1887 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1888 if($evt) { $__isrenewal = 0; return $evt; }
1891 ($patron, $copy, $evt) = _find_patron_from_params($params);
1892 return $evt if $evt;
1893 $params->{copy} = $copy;
1896 # verify our login session
1897 ($requestor, $evt) = $U->checkses($authtoken);
1898 if($evt) { $__isrenewal = 0; return $evt; }
1900 # make sure we have permission to perform a renewal
1901 if( $requestor->id ne $patron->id ) {
1902 $evt = $U->check_perms($requestor->id, $requestor->ws_ou, 'RENEW_CIRC');
1903 if($evt) { $__isrenewal = 0; return $evt; }
1907 # fetch and build the circulation environment
1908 ( $ctx, $evt ) = create_circ_ctx( %$params,
1910 requestor => $requestor,
1913 #fetch_patron_circ_summary => 1,
1914 fetch_copy_statuses => 1,
1915 fetch_copy_locations => 1,
1917 if($evt) { $__isrenewal = 0; return $evt; }
1918 $params->{_ctx} = $ctx;
1920 # make sure they have some renewals left and make sure the circulation exists
1921 ($circ, $evt) = _check_renewal_remaining($ctx);
1922 if($evt) { $__isrenewal = 0; return $evt; }
1923 $ctx->{old_circ} = $circ;
1924 my $renewals = $circ->renewal_remaining - 1;
1926 # run the renew permit script
1927 $evt = _run_renew_scripts($ctx);
1928 if($evt) { $__isrenewal = 0; return $evt; }
1931 #$ctx->{patron} = $ctx->{patron}->id;
1932 $evt = $self->generic_receive($client, $authtoken, $ctx );
1933 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1935 if( !$U->event_equals($evt, 'SUCCESS') ) {
1936 $__isrenewal = 0; return $evt;
1939 # re-fetch the context since objects have changed in the checkin
1940 ( $ctx, $evt ) = create_circ_ctx( %$params,
1942 requestor => $requestor,
1945 #fetch_patron_circ_summary => 1,
1946 fetch_copy_statuses => 1,
1947 fetch_copy_locations => 1,
1949 if($evt) { $__isrenewal = 0; return $evt; }
1950 $params->{_ctx} = $ctx;
1951 $ctx->{renewal_remaining} = $renewals;
1953 # run the circ permit scripts
1954 if( $ctx->{permit_override} ) {
1955 $evt = $U->check_perms(
1956 $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1957 if($evt) { $__isrenewal = 0; return $evt; }
1960 $evt = $self->permit_circ( $client, $authtoken, $params );
1961 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1962 #$ctx->{precat} = 1;
1963 $params->{precat} = 1;
1966 if(!$U->event_equals($evt, 'SUCCESS')) {
1967 if($evt) { $__isrenewal = 0; return $evt; }
1970 $params->{permit_key} = $evt->{payload};
1974 # checkout the item again
1975 $params->{patron} = $ctx->{patron}->id;
1976 $evt = $self->checkout($client, $authtoken, $params );
1978 $logger->activity("user ".$requestor->id." renewl of item ".
1979 $ctx->{copy}->barcode." completed with event ".$evt->{textcode});
1985 sub _check_renewal_remaining {
1988 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1989 return (undef, $evt) if $evt;
1990 $evt = OpenILS::Event->new(
1991 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1992 return ($circ, $evt);
1995 sub _run_renew_scripts {
1997 my $runner = $ctx->{runner};
2000 $runner->load($scripts{circ_permit_renew});
2001 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
2003 my $events = $runner->retrieve('result.events');
2004 $events = [ split(/,/, $events) ];
2005 $logger->activity("circ_permit_renew for user ".
2006 $ctx->{patron}->id." returned events: @$events") if @$events;
2009 push( @allevents, OpenILS::Event->new($_)) for @$events;
2010 return \@allevents if @allevents;