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 Digest::MD5 qw(md5_hex);
8 use OpenILS::Utils::ScriptRunner;
9 use OpenILS::Application::AppUtils;
10 use OpenILS::Application::Circ::Holds;
11 use OpenILS::Application::Circ::Transit;
12 use OpenILS::Utils::PermitHold;
13 use OpenSRF::Utils::Logger qw(:logger);
15 use DateTime::Format::ISO8601;
16 use OpenSRF::Utils qw/:datetime/;
18 $Data::Dumper::Indent = 0;
19 my $apputils = "OpenILS::Application::AppUtils";
21 my $holdcode = "OpenILS::Application::Circ::Holds";
22 my $transcode = "OpenILS::Application::Circ::Transit";
24 my %scripts; # - circulation script filenames
25 my $script_libs; # - any additional script libraries
26 my %cache; # - db objects cache
27 my %contexts; # - Script runner contexts
28 my $cache_handle; # - memcache handle
30 sub PRECAT_FINE_LEVEL { return 2; }
31 sub PRECAT_LOAN_DURATION { return 2; }
33 my %RECORD_FROM_COPY_CACHE;
36 # for security, this is a process-defined and not
37 # 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 '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 if(!defined($cache{patron_standings})) {
119 $cache{patron_standings} = $U->fetch_patron_standings();
120 $cache{group_tree} = $U->fetch_permission_group_tree();
123 $ctx->{patron_standings} = $cache{patron_standings};
124 $ctx->{group_tree} = $cache{group_tree};
126 $ctx->{patron_circ_summary} =
127 $U->fetch_patron_circ_summary($ctx->{patron}->id)
128 if $params{fetch_patron_circsummary};
134 sub _find_copy_by_attr {
139 my $copy = $params{copy} || undef;
144 $U->fetch_copy($params{copyid}) if $params{copyid};
145 return (undef,$evt) if $evt;
149 $U->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
150 return (undef,$evt) if $evt;
153 return ( $copy, $evt );
156 sub _ctx_add_copy_objects {
157 my($ctx, %params) = @_;
162 $cache{copy_statuses} = $U->fetch_copy_statuses
163 if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
165 $cache{copy_locations} = $U->fetch_copy_locations
166 if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
168 $ctx->{copy_statuses} = $cache{copy_statuses};
169 $ctx->{copy_locations} = $cache{copy_locations};
171 ($copy, $evt) = _find_copy_by_attr(%params);
174 if( $copy and !$ctx->{title} ) {
175 $logger->debug("Copy status: " . $copy->status);
177 my $r = $RECORD_FROM_COPY_CACHE{$copy->id};
178 ($r, $evt) = $U->fetch_record_by_copy( $copy->id ) unless $r;
180 $RECORD_FROM_COPY_CACHE{$copy->id} = $r;
183 $ctx->{copy} = $copy;
190 # ------------------------------------------------------------------------------
191 # Fleshes parts of the patron object
192 # ------------------------------------------------------------------------------
193 sub _doctor_copy_object {
196 my $copy = $ctx->{copy} || return undef;
198 $logger->debug("Doctoring copy object...");
200 # set the copy status to a status name
201 $copy->status( _get_copy_status( $copy, $ctx->{copy_statuses} ) );
203 # set the copy location to the location object
204 $copy->location( _get_copy_location( $copy, $ctx->{copy_locations} ) );
206 $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
210 # ------------------------------------------------------------------------------
211 # Fleshes parts of the patron object
212 # ------------------------------------------------------------------------------
213 sub _doctor_patron_object {
216 my $patron = $ctx->{patron} || return undef;
218 # push the standing object into the patron
219 if(ref($ctx->{patron_standings})) {
220 for my $s (@{$ctx->{patron_standings}}) {
221 if( $s->id eq $ctx->{patron}->standing ) {
222 $patron->standing($s);
223 $logger->debug("Set patron standing to ". $s->value);
228 # set the patron ptofile to the profile name
229 $patron->profile( _get_patron_profile(
230 $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
234 $U->fetch_org_unit( $patron->home_ou ) ) if $patron;
238 # recurse and find the patron profile name from the tree
239 # another option would be to grab the groups for the patron
240 # and cycle through those until the "profile" group has been found
241 sub _get_patron_profile {
242 my( $patron, $group_tree ) = @_;
243 return $group_tree if ($group_tree->id eq $patron->profile);
244 return undef unless ($group_tree->children);
246 for my $child (@{$group_tree->children}) {
247 my $ret = _get_patron_profile( $patron, $child );
253 sub _get_copy_status {
254 my( $copy, $cstatus ) = @_;
257 for my $status (@$cstatus) {
258 $s = $status if( $status->id eq $copy->status )
260 $logger->debug("Retrieving copy status: " . $s->name) if $s;
264 sub _get_copy_location {
265 my( $copy, $locations ) = @_;
268 for my $loc (@$locations) {
269 $l = $loc if $loc->id eq $copy->location;
271 $logger->debug("Retrieving copy location: " . $l->name ) if $l;
276 # ------------------------------------------------------------------------------
277 # Constructs and shoves data into the script environment
278 # ------------------------------------------------------------------------------
279 sub _build_circ_script_runner {
283 $logger->debug("Loading script environment for circulation");
286 if( $runner = $contexts{$ctx->{type}} ) {
287 $runner->refresh_context;
289 $runner = OpenILS::Utils::ScriptRunner->new;
290 $contexts{type} = $runner;
294 $logger->debug("Loading circ script lib path $_");
295 $runner->add_path( $_ );
298 # Note: inserting the number 0 into the script turns into the
299 # string "0", and thus evaluates to true in JS land
300 # inserting undef will insert "", which evaluates to false
302 $runner->insert( 'environment.patron', $ctx->{patron}, 1);
303 $runner->insert( 'environment.title', $ctx->{title}, 1);
304 $runner->insert( 'environment.copy', $ctx->{copy}, 1);
307 $runner->insert( 'result', {} );
308 #$runner->insert( 'result.event', 'SUCCESS' );
309 $runner->insert( 'result.events', [] );
312 $runner->insert('environment.isRenewal', 1);
314 $runner->insert('environment.isRenewal', undef);
317 if($ctx->{ishold} ) {
318 $runner->insert('environment.isHold', 1);
320 $runner->insert('environment.isHold', undef)
323 if( $ctx->{noncat} ) {
324 $runner->insert('environment.isNonCat', 1);
325 $runner->insert('environment.nonCatType', $ctx->{noncat_type});
327 $runner->insert('environment.isNonCat', undef);
330 if(ref($ctx->{patron_circ_summary})) {
331 $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
332 $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
335 $ctx->{runner} = $runner;
340 sub _add_script_runner_methods {
343 my $runner = $ctx->{runner};
347 # allows a script to fetch a hold that is currently targeting the
349 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
351 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
352 $hold = undef unless $hold;
353 $runner->insert( $key, $hold, 1 );
359 # ------------------------------------------------------------------------------
361 __PACKAGE__->register_method(
362 method => "permit_circ",
363 api_name => "open-ils.circ.checkout.permit",
365 Determines if the given checkout can occur
366 @param authtoken The login session key
367 @param params A trailing hash of named params including
368 barcode : The copy barcode,
369 patron : The patron the checkout is occurring for,
370 renew : true or false - whether or not this is a renewal
371 @return The event that occurred during the permit check.
374 __PACKAGE__->register_method (
375 method => 'permit_circ',
376 api_name => 'open-ils.circ.checkout.permit.override',
377 signature => q/@see open-ils.circ.checkout.permit/,
381 my( $self, $client, $authtoken, $params ) = @_;
384 my ( $requestor, $patron, $ctx, $evt, $circ );
386 my $override = ($self->api_name =~ /override/) ? 1 : 0;
388 # check permisson of the requestor
389 ( $requestor, $patron, $evt ) =
390 $U->checkses_requestor(
391 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
394 # fetch and build the circulation environment
395 if( !( $ctx = $params->{_ctx}) ) {
397 ( $ctx, $evt ) = create_circ_ctx( %$params,
399 requestor => $requestor,
401 fetch_patron_circ_summary => 1,
402 fetch_copy_statuses => 1,
403 fetch_copy_locations => 1,
408 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
409 ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
410 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
414 $ctx->{permit_key} = _cache_permit_key();
415 my $events = _run_permit_scripts($ctx);
418 $evt = override_events($requestor, $requestor->ws_ou, $events);
420 return OpenILS::Event->new('SUCCESS', payload => $ctx->{permit_key} );
426 sub override_events {
428 my( $requestor, $org, $events ) = @_;
429 $events = [ $events ] unless ref($events) eq 'ARRAY';
432 for my $e (@$events) {
433 my $tc = $e->{textcode};
434 next if $tc eq 'SUCCESS';
435 my $ov = "$tc.override";
436 my $evt = $U->check_perms( $requestor->id, $org, $ov );
444 __PACKAGE__->register_method(
445 method => "check_title_hold",
446 api_name => "open-ils.circ.title_hold.is_possible",
448 Determines if a hold were to be placed by a given user,
449 whether or not said hold would have any potential copies
451 @param authtoken The login session key
452 @param params A hash of named params including:
453 patronid - the id of the hold recipient
454 titleid (brn) - the id of the title to be held
455 depth - the hold range depth (defaults to 0)
458 sub check_title_hold {
459 my( $self, $client, $authtoken, $params ) = @_;
460 my %params = %$params;
461 my $titleid = $params{titleid};
463 my ( $requestor, $patron, $evt ) = $U->checkses_requestor(
464 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
467 my $rangelib = $patron->home_ou;
468 my $depth = $params{depth} || 0;
470 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
472 my $org = $U->simplereq(
474 'open-ils.actor.org_unit.retrieve',
475 $authtoken, $requestor->home_ou );
481 while( $title = $U->storagereq(
482 'open-ils.storage.biblio.record_entry.ranged_tree',
483 $titleid, $rangelib, $depth, $limit, $offset ) ) {
485 last unless ref($title);
487 for my $cn (@{$title->call_numbers}) {
489 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
491 for my $copy (@{$cn->copies}) {
493 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
495 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
497 requestor => $requestor,
500 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
501 request_lib => $org } );
503 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
515 # Runs the patron and copy permit scripts
516 # if this is a non-cat circulation, the copy permit script
518 sub _run_permit_scripts {
520 my $runner = $ctx->{runner};
521 my $patronid = $ctx->{patron}->id;
522 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
523 my $key = $ctx->{permit_key};
526 $runner->load($scripts{circ_permit_patron});
527 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
529 #my $evtname = $runner->retrieve('result.event');
531 # ---------------------------------------------------------------------
532 # Capture all of the patron permit events
533 # ---------------------------------------------------------------------
534 my $patron_events = $runner->retrieve('result.events');
535 $patron_events = [ split(/,/, $patron_events) ];
536 #$ctx->{circ_permit_patron_events} = $patron_events;
538 #$logger->activity("circ_permit_patron for user $patronid returned event: $evtname");
539 $logger->activity("circ_permit_patron for user $patronid returned events: @$patron_events");
541 #return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';
544 if( $ctx->{noncat} ) {
545 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
546 return OpenILS::Event->new('SUCCESS', payload => $key);
550 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
551 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
555 $logger->debug("Exiting circ permit early because request is for hold patron permit");
556 return OpenILS::Event->new('SUCCESS');
559 $runner->load($scripts{circ_permit_copy});
560 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
561 #$evtname = $runner->retrieve('result.event');
562 #$logger->activity("circ_permit_copy for user $patronid ".
563 #"and copy $barcode returned event: $evtname");
565 # ---------------------------------------------------------------------
566 # Capture all of the copy permit events
567 # ---------------------------------------------------------------------
568 my $copy_events = $runner->retrieve('result.events');
569 $copy_events = [ split(/,/, $copy_events) ];
570 $ctx->{circ_permit_copy_events} = $copy_events;
571 $logger->activity("circ_permit_copy for copy $barcode returned events: @$copy_events");
573 #return OpenILS::Event->new($evtname, payload => $key) if( $evtname eq 'SUCCESS' );
575 push( @allevents, OpenILS::Event->new($_)) for @$patron_events;
576 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
578 return OpenILS::Event->new('SUCCESS', payload => $key)
579 unless (@$copy_events or @$patron_events);
581 # uniquify the events
582 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
583 @allevents = values %hash;
587 # takes copyid, patronid, and requestor id
588 sub _cache_permit_key {
589 my $key = md5_hex( time() . rand() . "$$" );
590 $logger->debug("Setting circ permit key to $key");
591 $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
595 sub _check_permit_key {
597 $logger->debug("Fetching circ permit key $key");
598 my $k = "oils_permit_key_$key";
599 my $one = $cache_handle->get_cache($k);
600 $cache_handle->delete_cache($k);
601 return ($one) ? 1 : 0;
605 # ------------------------------------------------------------------------------
607 __PACKAGE__->register_method(
608 method => "checkout",
609 api_name => "open-ils.circ.checkout",
612 @param authtoken The login session key
613 @param params A named hash of params including:
615 barcode If no copy is provided, the copy is retrieved via barcode
616 copyid If no copy or barcode is provide, the copy id will be use
617 patron The patron's id
618 noncat True if this is a circulation for a non-cataloted item
619 noncat_type The non-cataloged type id
620 noncat_circ_lib The location for the noncat circ.
621 precat The item has yet to be cataloged
622 dummy_title The temporary title of the pre-cataloded item
623 dummy_author The temporary authr of the pre-cataloded item
624 Default is the home org of the staff member
625 @return The SUCCESS event on success, any other event depending on the error
629 my( $self, $client, $authtoken, $params ) = @_;
632 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
633 my $key = $params->{permit_key};
635 # if this is a renewal, then the requestor does not have to
636 # have checkout privelages
637 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
638 ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
641 if( $params->{patron} ) {
642 ( $patron, $evt ) = $U->fetch_user($params->{patron});
645 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
649 # set the circ lib to the home org of the requestor if not specified
650 my $circlib = (defined($params->{circ_lib})) ?
651 $params->{circ_lib} : $requestor->home_ou;
654 # Make sure the caller has a valid permit key or is
655 # overriding the permit can
656 if( $params->{permit_override} ) {
657 $evt = $U->check_perms(
658 $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
662 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
663 unless _check_permit_key($key);
666 # if this is a non-cataloged item, check it out and return
667 return _checkout_noncat(
668 $key, $requestor, $patron, %$params ) if $params->{noncat};
670 # if this item has yet to be cataloged, make sure a dummy copy exists
671 ( $params->{copy}, $evt ) = _make_precat_copy(
672 $requestor, $circlib, $params ) if $params->{precat};
676 # fetch and build the circulation environment
677 if( !( $ctx = $params->{_ctx}) ) {
678 ( $ctx, $evt ) = create_circ_ctx( %$params,
680 requestor => $requestor,
681 session => $U->start_db_session(),
683 fetch_patron_circ_summary => 1,
684 fetch_copy_statuses => 1,
685 fetch_copy_locations => 1,
689 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
691 # if the call doesn't know it's not cataloged..
692 if(!$params->{precat}) {
693 if( $ctx->{copy}->call_number eq '-1' ) {
694 return OpenILS::Event->new('ITEM_NOT_CATALOGED');
698 # this happens in permit.. but we need to check here for 'offline' requests
699 ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
700 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
702 my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
705 $ctx->{circ_lib} = $circlib;
707 $evt = _run_checkout_scripts($ctx);
711 _build_checkout_circ_object($ctx);
713 $evt = _apply_modified_due_date($ctx);
716 $evt = _commit_checkout_circ_object($ctx);
719 $evt = _update_checkout_copy($ctx);
723 ($holds, $evt) = _handle_related_holds($ctx);
727 $logger->debug("Checkin committing objects with session thread trace: ".$ctx->{session}->session_id);
728 $U->commit_db_session($ctx->{session});
729 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
731 return OpenILS::Event->new('SUCCESS',
733 copy => $U->unflesh_copy($ctx->{copy}),
734 circ => $ctx->{circ},
736 holds_fulfilled => $holds,
741 sub _make_precat_copy {
742 my ( $requestor, $circlib, $params ) = @_;
744 my( $copy, undef ) = _find_copy_by_attr(%$params);
747 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
748 return ($copy, undef);
751 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
753 my $evt = OpenILS::Event->new(
754 'BAD_PARAMS', desc => "Dummy title or author not provided" )
755 unless ( $params->{dummy_title} and $params->{dummy_author} );
756 return (undef, $evt) if $evt;
758 $copy = Fieldmapper::asset::copy->new;
759 $copy->circ_lib($circlib);
760 $copy->creator($requestor->id);
761 $copy->editor($requestor->id);
762 $copy->barcode($params->{barcode});
763 $copy->call_number(-1); #special CN for precat materials
764 $copy->loan_duration(&PRECAT_LOAN_DURATION);
765 $copy->fine_level(&PRECAT_FINE_LEVEL);
767 $copy->dummy_title($params->{dummy_title});
768 $copy->dummy_author($params->{dummy_author});
770 my $id = $U->storagereq(
771 'open-ils.storage.direct.asset.copy.create', $copy );
772 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
774 $logger->debug("Pre-cataloged copy successfully created");
775 return $U->fetch_copy($id);
779 sub _run_checkout_scripts {
785 my $runner = $ctx->{runner};
787 $runner->insert('result.durationLevel');
788 $runner->insert('result.durationRule');
789 $runner->insert('result.recurringFinesRule');
790 $runner->insert('result.recurringFinesLevel');
791 $runner->insert('result.maxFine');
793 $runner->load($scripts{circ_duration});
794 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
795 my $duration = $runner->retrieve('result.durationRule');
796 $logger->debug("Circ duration script yielded a duration rule of: $duration");
798 $runner->load($scripts{circ_recurring_fines});
799 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
800 my $recurring = $runner->retrieve('result.recurringFinesRule');
801 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
803 $runner->load($scripts{circ_max_fines});
804 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
805 my $max_fine = $runner->retrieve('result.maxFine');
806 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
808 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
810 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
812 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
815 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
816 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
817 $ctx->{duration_rule} = $duration;
818 $ctx->{recurring_fines_rule} = $recurring;
819 $ctx->{max_fine_rule} = $max_fine;
824 sub _build_checkout_circ_object {
828 my $circ = new Fieldmapper::action::circulation;
829 my $duration = $ctx->{duration_rule};
830 my $max = $ctx->{max_fine_rule};
831 my $recurring = $ctx->{recurring_fines_rule};
832 my $copy = $ctx->{copy};
833 my $patron = $ctx->{patron};
834 my $dur_level = $ctx->{duration_level};
835 my $rec_level = $ctx->{recurring_fines_level};
837 $circ->duration( $duration->shrt ) if ($dur_level == 1);
838 $circ->duration( $duration->normal ) if ($dur_level == 2);
839 $circ->duration( $duration->extended ) if ($dur_level == 3);
841 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
842 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
843 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
845 $circ->duration_rule( $duration->name );
846 $circ->recuring_fine_rule( $recurring->name );
847 $circ->max_fine_rule( $max->name );
848 $circ->max_fine( $max->amount );
850 $circ->fine_interval($recurring->recurance_interval);
851 $circ->renewal_remaining( $duration->max_renewals );
852 $circ->target_copy( $copy->id );
853 $circ->usr( $patron->id );
854 $circ->circ_lib( $ctx->{circ_lib} );
857 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
858 $circ->opac_renewal(1);
859 $circ->renewal_remaining($ctx->{renewal_remaining});
860 $circ->circ_staff($ctx->{requestor}->id);
864 # if the user provided an overiding checkout time,
865 # (e.g. the checkout really happened several hours ago), then
866 # we apply that here. Does this need a perm??
867 if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
868 $logger->debug("circ setting checkout_time to $ds");
869 $circ->xact_start($ds);
872 # if a patron is renewing, 'requestor' will be the patron
873 $circ->circ_staff($ctx->{requestor}->id );
874 _set_circ_due_date($circ);
875 $ctx->{circ} = $circ;
878 sub _apply_modified_due_date {
880 my $circ = $ctx->{circ};
882 if( $ctx->{due_date} ) {
884 my $evt = $U->check_perms(
885 $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
888 my $ds = _create_date_stamp($ctx->{due_date});
889 $logger->debug("circ modifying due_date to $ds");
890 $circ->due_date($ds);
896 sub _create_date_stamp {
897 my $datestring = shift;
898 return undef unless $datestring;
899 $datestring = clense_ISO8601($datestring);
900 $logger->debug("circ created date stamp => $datestring");
904 sub _create_due_date {
905 my $duration = shift;
907 my ($sec,$min,$hour,$mday,$mon,$year) =
908 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
909 $year += 1900; $mon += 1;
910 my $due_date = sprintf(
911 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
912 $year, $mon, $mday, $hour, $min, $sec);
916 sub _set_circ_due_date {
919 my $dd = _create_due_date($circ->duration);
920 $logger->debug("Checkout setting due date on circ to: $dd");
921 $circ->due_date($dd);
924 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
925 sub _update_checkout_copy {
928 my $copy = $ctx->{copy};
930 my $s = $U->copy_status_from_name('checked out');
931 $copy->status( $s->id ) if $s;
933 my $evt = $U->update_copy( session => $ctx->{session},
934 copy => $copy, editor => $ctx->{requestor}->id );
935 return (undef,$evt) if $evt;
940 # commits the circ object to the db then fleshes the circ with rules objects
941 sub _commit_checkout_circ_object {
944 my $circ = $ctx->{circ};
948 my $r = $ctx->{session}->request(
949 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
951 return $U->DB_UPDATE_FAILED($circ) unless $r;
953 $logger->debug("Created a new circ object in checkout: $r");
956 $circ->duration_rule($ctx->{duration_rule});
957 $circ->max_fine_rule($ctx->{max_fine_rule});
958 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
964 # sees if there are any holds that this copy
965 sub _handle_related_holds {
968 my $copy = $ctx->{copy};
969 my $patron = $ctx->{patron};
970 my $holds = $holdcode->fetch_related_holds($copy->id);
974 # XXX We should only fulfill one hold here...
975 # XXX If a hold was transited to the user who is checking out
976 # the item, we need to make sure that hold is what's grabbed
977 if(ref($holds) && @$holds) {
979 # for now, just sort by id to get what should be the oldest hold
980 $holds = [ sort { $a->id <=> $b->id } @$holds ];
981 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
984 my $hold = $holds->[0];
986 $logger->debug("Related hold found in checkout: " . $hold->id );
988 $hold->current_copy($copy->id); # just make sure it's set
989 # if the hold was never officially captured, capture it.
990 $hold->capture_time('now') unless $hold->capture_time;
991 $hold->fulfillment_time('now');
992 my $r = $ctx->{session}->request(
993 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
994 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
995 push( @fulfilled, $hold->id );
999 return (\@fulfilled, undef);
1002 sub _checkout_noncat {
1003 my ( $key, $requestor, $patron, %params ) = @_;
1004 my( $circ, $circlib, $evt );
1007 $circlib = $params{noncat_circ_lib} || $requestor->home_ou;
1009 my $count = $params{noncat_count} || 1;
1010 my $cotime = _create_date_stamp($params{checkout_time}) || "";
1011 $logger->info("circ creating $count noncat circs with checkout time $cotime");
1013 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
1014 $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
1015 return $evt if $evt;
1018 return OpenILS::Event->new(
1019 'SUCCESS', payload => { noncat_circ => $circ } );
1023 __PACKAGE__->register_method(
1024 method => "generic_receive",
1025 api_name => "open-ils.circ.checkin",
1028 Generic super-method for handling all copies
1029 @param authtoken The login session key
1030 @param params Hash of named parameters including:
1031 barcode - The copy barcode
1032 force - If true, copies in bad statuses will be checked in and give good statuses
1037 sub generic_receive {
1038 my( $self, $connection, $authtoken, $params ) = @_;
1039 my( $ctx, $requestor, $evt );
1041 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1042 ( $requestor, $evt ) = $U->checksesperm(
1043 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1044 return $evt if $evt;
1046 # load up the circ objects
1047 if( !( $ctx = $params->{_ctx}) ) {
1048 ( $ctx, $evt ) = create_circ_ctx( %$params,
1049 requestor => $requestor,
1050 session => $U->start_db_session(),
1052 fetch_copy_statuses => 1,
1053 fetch_copy_locations => 1,
1056 return $evt if $evt;
1058 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1059 $ctx->{authtoken} = $authtoken;
1060 my $session = $ctx->{session};
1062 my $copy = $ctx->{copy};
1063 $U->unflesh_copy($copy);
1064 return OpenILS::Event->new('COPY_NOT_FOUND') unless $copy;
1066 $logger->info("Checkin copy called by user ".
1067 $requestor->id." for copy ".$copy->id);
1069 return $self->checkin_do_receive($connection, $ctx);
1072 sub checkin_do_receive {
1074 my( $self, $connection, $ctx ) = @_;
1077 my $copy = $ctx->{copy};
1078 my $session = $ctx->{session};
1079 my $requestor = $ctx->{requestor};
1080 my $change = 0; # did we actually do anything?
1082 if(!$ctx->{force}) {
1083 return $evt if ($evt = _checkin_check_copy_status($copy));
1086 ($ctx->{circ}, $evt) = $U->fetch_open_circulation($copy->id);
1087 return $evt if ($evt and $__isrenewal); # renewals require a circulation
1089 ($ctx->{transit}) = $U->fetch_open_transit_by_copy($copy->id);
1091 if( $ctx->{circ} ) {
1093 # There is an open circ on this item, close it out.
1095 $evt = _checkin_handle_circ($ctx);
1096 return $evt if $evt;
1098 } elsif( $ctx->{transit} ) {
1100 # is this item currently in transit?
1102 $evt = $transcode->transit_receive( $copy, $requestor, $session );
1103 my $holdtrans = $evt->{holdtransit};
1104 ($ctx->{hold}) = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1106 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1108 # either an error occurred or a ROUTE_ITEM was generated and the
1109 # item must be forwarded on to its destination.
1110 return _checkin_flesh_event($ctx, $evt);
1116 # copy was received as a hold transit. Copy is at target lib
1117 # and hold transit is complete. We're done here...
1118 $U->commit_db_session($session);
1119 return _checkin_flesh_event($ctx, $evt);
1125 # ------------------------------------------------------------------------------
1126 # Circulations and transits are now closed where necessary. Now go on to see if
1127 # this copy can fulfill a hold or needs to be routed to a different location
1128 # ------------------------------------------------------------------------------
1131 # If it's a renewal, we're done
1133 $U->commit_db_session($session);
1134 return OpenILS::Event->new('SUCCESS');
1138 # Now, let's see if this copy is needed for a hold
1139 my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor );
1143 $ctx->{hold} = $hold;
1146 # Capture the hold with this copy
1147 return $evt if ($evt = _checkin_capture_hold($ctx));
1149 if( $hold->pickup_lib == $requestor->home_ou ) {
1151 # This hold was captured in the correct location
1152 $evt = OpenILS::Event->new('SUCCESS');
1156 # Hold needs to be picked up elsewhere. Build a hold
1157 # transit and route the item.
1158 return $evt if ($evt =_checkin_build_hold_transit($ctx));
1159 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1162 } else { # not needed for a hold
1164 if( $copy->circ_lib == $requestor->home_ou ) {
1166 # Copy is in the right place.
1167 $evt = OpenILS::Event->new('SUCCESS');
1169 # if the item happens to be a pre-cataloged item, send it
1170 # to cataloging and return the event
1171 my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1172 return $err if $err;
1178 # Copy wants to go home. Transit it there.
1179 return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1180 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1185 $logger->info("Copy checkin finished with event: ".$evt->{textcode});
1189 $evt = OpenILS::Event->new('NO_CHANGE');
1190 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id)
1191 if ($copy->status == $U->copy_status_from_name('on holds shelf')->id);
1195 $U->commit_db_session($session);
1198 return _checkin_flesh_event($ctx, $evt);
1201 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1202 sub _checkin_handle_precat {
1205 my $copy = $ctx->{copy};
1210 my $catstat = $U->copy_status_from_name('cataloging');
1212 if( $ctx->{precat} ) {
1214 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1216 if( $copy->status != $catstat->id ) {
1217 $copy->status($catstat->id);
1219 return (undef, 0, $errevt) if (
1220 $errevt = $U->update_copy(
1222 editor => $ctx->{requestor}->id,
1223 session => $ctx->{session} ));
1229 return ($evt, $change, undef);
1233 sub _checkin_check_copy_status {
1235 my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1236 my $evt = OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1237 return $evt if ($stat == $U->copy_status_from_name('lost')->id);
1238 return $evt if ($stat == $U->copy_status_from_name('missing')->id);
1242 # Just gets the copy back home. Returns undef on success, event on error
1243 sub _checkin_build_generic_copy_transit {
1246 my $requestor = $ctx->{requestor};
1247 my $copy = $ctx->{copy};
1248 my $transit = Fieldmapper::action::transit_copy->new;
1249 my $session = $ctx->{session};
1251 $logger->activity("User ". $requestor->id ." creating a ".
1252 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1254 $transit->source($requestor->home_ou);
1255 $transit->dest($copy->circ_lib);
1256 $transit->target_copy($copy->id);
1257 $transit->source_send_time('now');
1258 $transit->copy_status($copy->status);
1260 $logger->debug("Creating new copy_transit in DB");
1262 my $s = $session->request(
1263 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1264 return $U->DB_UPDATE_FAILED($transit) unless $s;
1266 $logger->info("Checkin copy successfully created new transit: $s");
1268 $copy->status($U->copy_status_from_name('in transit')->id );
1270 return $U->update_copy( copy => $copy,
1271 editor => $requestor->id, session => $session );
1276 # returns event on error, undef on success
1277 sub _checkin_build_hold_transit {
1280 my $copy = $ctx->{copy};
1281 my $hold = $ctx->{hold};
1282 my $trans = Fieldmapper::action::hold_transit_copy->new;
1284 $trans->hold($hold->id);
1285 $trans->source($ctx->{requestor}->home_ou);
1286 $trans->dest($hold->pickup_lib);
1287 $trans->source_send_time("now");
1288 $trans->target_copy($copy->id);
1289 $trans->copy_status($copy->status);
1291 my $id = $ctx->{session}->request(
1292 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1293 return $U->DB_UPDATE_FAILED($trans) unless $id;
1295 $logger->info("Checkin copy successfully created hold transit: $id");
1297 $copy->status($U->copy_status_from_name('in transit')->id );
1298 return $U->update_copy( copy => $copy,
1299 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1302 # Returns event on error, undef on success
1303 sub _checkin_capture_hold {
1305 my $copy = $ctx->{copy};
1306 my $hold = $ctx->{hold};
1308 $logger->debug("Checkin copy capturing hold ".$hold->id);
1310 $hold->current_copy($copy->id);
1311 $hold->capture_time('now');
1313 my $stat = $ctx->{session}->request(
1314 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1315 return $U->DB_UPDATE_FAILED($hold) unless $stat;
1317 $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1319 return $U->update_copy( copy => $copy,
1320 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1323 # fleshes an event with the relevant objects from the context
1324 sub _checkin_flesh_event {
1329 $payload->{copy} = $U->unflesh_copy($ctx->{copy});
1330 $payload->{record} = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1331 $payload->{circ} = $ctx->{circ} if $ctx->{circ};
1332 $payload->{transit} = $ctx->{transit} if $ctx->{transit};
1333 $payload->{hold} = $ctx->{hold} if $ctx->{hold};
1335 $evt->{payload} = $payload;
1340 # Closes out the circulation, puts the copy into reshelving.
1341 # Voids any bills attached to this circ after the backdate time
1342 # if a backdate is provided
1343 sub _checkin_handle_circ {
1347 my $circ = $ctx->{circ};
1348 my $copy = $ctx->{copy};
1349 my $requestor = $ctx->{requestor};
1350 my $session = $ctx->{session};
1352 $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1354 $ctx->{longoverdue} = 1 if ($circ->stop_fines =~ /longoverdue/io);
1355 $ctx->{claimsreturned} = 1 if ($circ->stop_fines =~ /claimsreturned/io);
1357 my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1358 return $evt if $evt;
1360 $circ->stop_fines('CHECKIN');
1361 $circ->stop_fines('RENEW') if $__isrenewal;
1362 $circ->stop_fines('LOST') if($__islost);
1363 $circ->xact_finish('now') if($obt->balance_owed <= 0 and !$__islost);
1364 $circ->stop_fines_time('now');
1365 $circ->checkin_time('now');
1366 $circ->checkin_staff($requestor->id);
1368 if(my $backdate = $ctx->{backdate}) {
1369 return $evt if ($evt =
1370 _checkin_handle_backdate($backdate, $circ, $requestor, $session));
1373 $logger->info("Checkin copy setting status to 'reshelving' and committing...");
1374 $copy->status($U->copy_status_from_name('reshelving')->id);
1375 $evt = $U->update_copy( session => $session,
1376 copy => $copy, editor => $requestor->id );
1377 return $evt if $evt;
1379 $ctx->{session}->request(
1380 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1385 # returns event on error, undef on success
1386 # This voids all bills attached to the given circulation that occurred
1387 # after the backdate
1388 sub _checkin_handle_backdate {
1389 my( $backdate, $circ, $requestor, $session ) = @_;
1391 $logger->activity("User ".$requestor->id.
1392 " backdating circ [".$circ->target_copy."] to date: $backdate");
1394 $circ->xact_finish($backdate);
1396 my $bills = $session->request( # XXX Verify this call is correct
1397 "open-ils.storage.direct.money.billing.search_where.atomic",
1398 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1401 for my $bill (@$bills) {
1403 my $s = $session->request(
1404 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1405 return $U->DB_UPDATE_FAILED($bill) unless $s;
1414 # ------------------------------------------------------------------------------
1416 __PACKAGE__->register_method(
1418 api_name => "open-ils.circ.renew",
1419 notes => <<" NOTES");
1420 PARAMS( authtoken, circ => circ_id );
1421 open-ils.circ.renew(login_session, circ_object);
1422 Renews the provided circulation. login_session is the requestor of the
1423 renewal and if the logged in user is not the same as circ->usr, then
1424 the logged in user must have RENEW_CIRC permissions.
1428 my( $self, $client, $authtoken, $params ) = @_;
1431 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1434 # fetch the patron object one way or another
1435 if( $params->{patron} ) {
1436 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1437 if($evt) { $__isrenewal = 0; return $evt; }
1439 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1440 if($evt) { $__isrenewal = 0; return $evt; }
1443 # verify our login session
1444 ($requestor, $evt) = $U->checkses($authtoken);
1445 if($evt) { $__isrenewal = 0; return $evt; }
1447 # make sure we have permission to perform a renewal
1448 if( $requestor->id ne $patron->id ) {
1449 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'RENEW_CIRC');
1450 if($evt) { $__isrenewal = 0; return $evt; }
1454 # fetch and build the circulation environment
1455 ( $ctx, $evt ) = create_circ_ctx( %$params,
1457 requestor => $requestor,
1460 fetch_patron_circ_summary => 1,
1461 fetch_copy_statuses => 1,
1462 fetch_copy_locations => 1,
1464 if($evt) { $__isrenewal = 0; return $evt; }
1465 $params->{_ctx} = $ctx;
1467 # make sure they have some renewals left and make sure the circulation exists
1468 ($circ, $evt) = _check_renewal_remaining($ctx);
1469 if($evt) { $__isrenewal = 0; return $evt; }
1470 $ctx->{old_circ} = $circ;
1471 my $renewals = $circ->renewal_remaining - 1;
1473 # run the renew permit script
1474 $evt = _run_renew_scripts($ctx);
1475 if($evt) { $__isrenewal = 0; return $evt; }
1478 #$ctx->{patron} = $ctx->{patron}->id;
1479 $evt = $self->generic_receive($client, $authtoken, $ctx );
1480 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1482 if( !$U->event_equals($evt, 'SUCCESS') ) {
1483 $__isrenewal = 0; return $evt;
1486 # re-fetch the context since objects have changed in the checkin
1487 ( $ctx, $evt ) = create_circ_ctx( %$params,
1489 requestor => $requestor,
1492 fetch_patron_circ_summary => 1,
1493 fetch_copy_statuses => 1,
1494 fetch_copy_locations => 1,
1496 if($evt) { $__isrenewal = 0; return $evt; }
1497 $params->{_ctx} = $ctx;
1498 $ctx->{renewal_remaining} = $renewals;
1500 # run the circ permit scripts
1501 if( $ctx->{permit_override} ) {
1502 $evt = $U->check_perms(
1503 $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1504 if($evt) { $__isrenewal = 0; return $evt; }
1507 $evt = $self->permit_circ( $client, $authtoken, $params );
1508 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1512 if(!$U->event_equals($evt, 'SUCCESS')) {
1513 if($evt) { $__isrenewal = 0; return $evt; }
1516 $params->{permit_key} = $evt->{payload};
1520 # checkout the item again
1521 $evt = $self->checkout($client, $authtoken, $params );
1527 sub _check_renewal_remaining {
1530 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1531 return (undef, $evt) if $evt;
1532 $evt = OpenILS::Event->new(
1533 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1534 return ($circ, $evt);
1537 sub _run_renew_scripts {
1539 my $runner = $ctx->{runner};
1542 $runner->load($scripts{circ_permit_renew});
1543 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1544 #my $evtname = $runner->retrieve('result.event');
1545 #$logger->activity("circ_permit_renew for user ".$ctx->{patron}->id." returned event: $evtname");
1547 my $events = $runner->retrieve('result.events');
1548 $events = [ split(/,/, $events) ];
1549 $logger->activity("circ_permit_renew for user ".$ctx->{patron}->id." returned events: @$events");
1552 push( @allevents, OpenILS::Event->new($_)) for @$events;
1553 return \@allevents if @allevents;
1555 #return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';