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.
375 my( $self, $client, $authtoken, $params ) = @_;
378 my ( $requestor, $patron, $ctx, $evt, $circ );
380 # check permisson of the requestor
381 ( $requestor, $patron, $evt ) =
382 $U->checkses_requestor(
383 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
386 # fetch and build the circulation environment
387 if( !( $ctx = $params->{_ctx}) ) {
389 ( $ctx, $evt ) = create_circ_ctx( %$params,
391 requestor => $requestor,
393 fetch_patron_circ_summary => 1,
394 fetch_copy_statuses => 1,
395 fetch_copy_locations => 1,
400 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
401 ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
402 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
405 return _run_permit_scripts($ctx);
409 __PACKAGE__->register_method(
410 method => "check_title_hold",
411 api_name => "open-ils.circ.title_hold.is_possible",
413 Determines if a hold were to be placed by a given user,
414 whether or not said hold would have any potential copies
416 @param authtoken The login session key
417 @param params A hash of named params including:
418 patronid - the id of the hold recipient
419 titleid (brn) - the id of the title to be held
420 depth - the hold range depth (defaults to 0)
423 sub check_title_hold {
424 my( $self, $client, $authtoken, $params ) = @_;
425 my %params = %$params;
426 my $titleid = $params{titleid};
428 my ( $requestor, $patron, $evt ) = $U->checkses_requestor(
429 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
432 my $rangelib = $patron->home_ou;
433 my $depth = $params{depth} || 0;
435 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
437 my $org = $U->simplereq(
439 'open-ils.actor.org_unit.retrieve',
440 $authtoken, $requestor->home_ou );
446 while( $title = $U->storagereq(
447 'open-ils.storage.biblio.record_entry.ranged_tree',
448 $titleid, $rangelib, $depth, $limit, $offset ) ) {
450 last unless ref($title);
452 for my $cn (@{$title->call_numbers}) {
454 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
456 for my $copy (@{$cn->copies}) {
458 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
460 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
462 requestor => $requestor,
465 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
466 request_lib => $org } );
468 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
480 # Runs the patron and copy permit scripts
481 # if this is a non-cat circulation, the copy permit script
483 sub _run_permit_scripts {
485 my $runner = $ctx->{runner};
486 my $patronid = $ctx->{patron}->id;
487 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
490 $runner->load($scripts{circ_permit_patron});
491 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
493 #my $evtname = $runner->retrieve('result.event');
495 # ---------------------------------------------------------------------
496 # Capture all of the patron permit events
497 # ---------------------------------------------------------------------
498 my $patron_events = $runner->retrieve('result.events');
499 $patron_events = [ split(/,/, $patron_events) ];
500 #$ctx->{circ_permit_patron_events} = $patron_events;
502 #$logger->activity("circ_permit_patron for user $patronid returned event: $evtname");
503 $logger->activity("circ_permit_patron for user $patronid returned events: @$patron_events");
505 #return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';
507 my $key = _cache_permit_key();
509 if( $ctx->{noncat} ) {
510 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
511 return OpenILS::Event->new('SUCCESS', payload => $key);
515 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
516 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
520 $logger->debug("Exiting circ permit early because request is for hold patron permit");
521 return OpenILS::Event->new('SUCCESS');
524 $runner->load($scripts{circ_permit_copy});
525 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
526 #$evtname = $runner->retrieve('result.event');
527 #$logger->activity("circ_permit_copy for user $patronid ".
528 #"and copy $barcode returned event: $evtname");
530 # ---------------------------------------------------------------------
531 # Capture all of the copy permit events
532 # ---------------------------------------------------------------------
533 my $copy_events = $runner->retrieve('result.events');
534 $copy_events = [ split(/,/, $copy_events) ];
535 $ctx->{circ_permit_copy_events} = $copy_events;
536 $logger->activity("circ_permit_copy for copy $barcode returned events: @$copy_events");
538 #return OpenILS::Event->new($evtname, payload => $key) if( $evtname eq 'SUCCESS' );
540 push( @allevents, OpenILS::Event->new($_)) for @$patron_events;
541 push( @allevents, OpenILS::Event->new($_)) for @$copy_events;
543 return OpenILS::Event->new('SUCCESS', payload => $key)
544 unless (@$copy_events or @$patron_events);
546 # uniquify the events
547 my %hash = map { ($_->{ilsevent} => $_) } @allevents;
548 @allevents = values %hash;
552 # takes copyid, patronid, and requestor id
553 sub _cache_permit_key {
554 my $key = md5_hex( time() . rand() . "$$" );
555 $logger->debug("Setting circ permit key to $key");
556 $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
560 sub _check_permit_key {
562 $logger->debug("Fetching circ permit key $key");
563 my $k = "oils_permit_key_$key";
564 my $one = $cache_handle->get_cache($k);
565 $cache_handle->delete_cache($k);
566 return ($one) ? 1 : 0;
570 # ------------------------------------------------------------------------------
572 __PACKAGE__->register_method(
573 method => "checkout",
574 api_name => "open-ils.circ.checkout",
577 @param authtoken The login session key
578 @param params A named hash of params including:
580 barcode If no copy is provided, the copy is retrieved via barcode
581 copyid If no copy or barcode is provide, the copy id will be use
582 patron The patron's id
583 noncat True if this is a circulation for a non-cataloted item
584 noncat_type The non-cataloged type id
585 noncat_circ_lib The location for the noncat circ.
586 precat The item has yet to be cataloged
587 dummy_title The temporary title of the pre-cataloded item
588 dummy_author The temporary authr of the pre-cataloded item
589 Default is the home org of the staff member
590 @return The SUCCESS event on success, any other event depending on the error
594 my( $self, $client, $authtoken, $params ) = @_;
597 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
598 my $key = $params->{permit_key};
600 # if this is a renewal, then the requestor does not have to
601 # have checkout privelages
602 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
603 ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
606 if( $params->{patron} ) {
607 ( $patron, $evt ) = $U->fetch_user($params->{patron});
610 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
614 # set the circ lib to the home org of the requestor if not specified
615 my $circlib = (defined($params->{circ_lib})) ?
616 $params->{circ_lib} : $requestor->home_ou;
619 # Make sure the caller has a valid permit key or is
620 # overriding the permit can
621 if( $params->{permit_override} ) {
622 $evt = $U->check_perms(
623 $requestor->id, $requestor->ws_ou, 'CIRC_PERMIT_OVERRIDE');
627 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
628 unless _check_permit_key($key);
631 # if this is a non-cataloged item, check it out and return
632 return _checkout_noncat(
633 $key, $requestor, $patron, %$params ) if $params->{noncat};
635 # if this item has yet to be cataloged, make sure a dummy copy exists
636 ( $params->{copy}, $evt ) = _make_precat_copy(
637 $requestor, $circlib, $params ) if $params->{precat};
641 # fetch and build the circulation environment
642 if( !( $ctx = $params->{_ctx}) ) {
643 ( $ctx, $evt ) = create_circ_ctx( %$params,
645 requestor => $requestor,
646 session => $U->start_db_session(),
648 fetch_patron_circ_summary => 1,
649 fetch_copy_statuses => 1,
650 fetch_copy_locations => 1,
654 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
656 # if the call doesn't know it's not cataloged..
657 if(!$params->{precat}) {
658 if( $ctx->{copy}->call_number eq '-1' ) {
659 return OpenILS::Event->new('ITEM_NOT_CATALOGED');
663 # this happens in permit.. but we need to check here for 'offline' requests
664 ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
665 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
667 my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
670 $ctx->{circ_lib} = $circlib;
672 $evt = _run_checkout_scripts($ctx);
676 _build_checkout_circ_object($ctx);
678 $evt = _apply_modified_due_date($ctx);
681 $evt = _commit_checkout_circ_object($ctx);
684 $evt = _update_checkout_copy($ctx);
688 ($holds, $evt) = _handle_related_holds($ctx);
692 $logger->debug("Checkin committing objects with session thread trace: ".$ctx->{session}->session_id);
693 $U->commit_db_session($ctx->{session});
694 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
696 return OpenILS::Event->new('SUCCESS',
698 copy => $U->unflesh_copy($ctx->{copy}),
699 circ => $ctx->{circ},
701 holds_fulfilled => $holds,
706 sub _make_precat_copy {
707 my ( $requestor, $circlib, $params ) = @_;
709 my( $copy, undef ) = _find_copy_by_attr(%$params);
712 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
713 return ($copy, undef);
716 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
718 my $evt = OpenILS::Event->new(
719 'BAD_PARAMS', desc => "Dummy title or author not provided" )
720 unless ( $params->{dummy_title} and $params->{dummy_author} );
721 return (undef, $evt) if $evt;
723 $copy = Fieldmapper::asset::copy->new;
724 $copy->circ_lib($circlib);
725 $copy->creator($requestor->id);
726 $copy->editor($requestor->id);
727 $copy->barcode($params->{barcode});
728 $copy->call_number(-1); #special CN for precat materials
729 $copy->loan_duration(&PRECAT_LOAN_DURATION);
730 $copy->fine_level(&PRECAT_FINE_LEVEL);
732 $copy->dummy_title($params->{dummy_title});
733 $copy->dummy_author($params->{dummy_author});
735 my $id = $U->storagereq(
736 'open-ils.storage.direct.asset.copy.create', $copy );
737 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
739 $logger->debug("Pre-cataloged copy successfully created");
740 return $U->fetch_copy($id);
744 sub _run_checkout_scripts {
750 my $runner = $ctx->{runner};
752 $runner->insert('result.durationLevel');
753 $runner->insert('result.durationRule');
754 $runner->insert('result.recurringFinesRule');
755 $runner->insert('result.recurringFinesLevel');
756 $runner->insert('result.maxFine');
758 $runner->load($scripts{circ_duration});
759 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
760 my $duration = $runner->retrieve('result.durationRule');
761 $logger->debug("Circ duration script yielded a duration rule of: $duration");
763 $runner->load($scripts{circ_recurring_fines});
764 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
765 my $recurring = $runner->retrieve('result.recurringFinesRule');
766 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
768 $runner->load($scripts{circ_max_fines});
769 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
770 my $max_fine = $runner->retrieve('result.maxFine');
771 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
773 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
775 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
777 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
780 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
781 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
782 $ctx->{duration_rule} = $duration;
783 $ctx->{recurring_fines_rule} = $recurring;
784 $ctx->{max_fine_rule} = $max_fine;
789 sub _build_checkout_circ_object {
793 my $circ = new Fieldmapper::action::circulation;
794 my $duration = $ctx->{duration_rule};
795 my $max = $ctx->{max_fine_rule};
796 my $recurring = $ctx->{recurring_fines_rule};
797 my $copy = $ctx->{copy};
798 my $patron = $ctx->{patron};
799 my $dur_level = $ctx->{duration_level};
800 my $rec_level = $ctx->{recurring_fines_level};
802 $circ->duration( $duration->shrt ) if ($dur_level == 1);
803 $circ->duration( $duration->normal ) if ($dur_level == 2);
804 $circ->duration( $duration->extended ) if ($dur_level == 3);
806 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
807 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
808 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
810 $circ->duration_rule( $duration->name );
811 $circ->recuring_fine_rule( $recurring->name );
812 $circ->max_fine_rule( $max->name );
813 $circ->max_fine( $max->amount );
815 $circ->fine_interval($recurring->recurance_interval);
816 $circ->renewal_remaining( $duration->max_renewals );
817 $circ->target_copy( $copy->id );
818 $circ->usr( $patron->id );
819 $circ->circ_lib( $ctx->{circ_lib} );
822 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
823 $circ->opac_renewal(1);
824 $circ->renewal_remaining($ctx->{renewal_remaining});
825 $circ->circ_staff($ctx->{requestor}->id);
829 # if the user provided an overiding checkout time,
830 # (e.g. the checkout really happened several hours ago), then
831 # we apply that here. Does this need a perm??
832 if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
833 $logger->debug("circ setting checkout_time to $ds");
834 $circ->xact_start($ds);
837 # if a patron is renewing, 'requestor' will be the patron
838 $circ->circ_staff($ctx->{requestor}->id );
839 _set_circ_due_date($circ);
840 $ctx->{circ} = $circ;
843 sub _apply_modified_due_date {
845 my $circ = $ctx->{circ};
847 if( $ctx->{due_date} ) {
849 my $evt = $U->check_perms(
850 $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
853 my $ds = _create_date_stamp($ctx->{due_date});
854 $logger->debug("circ modifying due_date to $ds");
855 $circ->due_date($ds);
861 sub _create_date_stamp {
862 my $datestring = shift;
863 return undef unless $datestring;
864 $datestring = clense_ISO8601($datestring);
865 $logger->debug("circ created date stamp => $datestring");
869 sub _create_due_date {
870 my $duration = shift;
872 my ($sec,$min,$hour,$mday,$mon,$year) =
873 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
874 $year += 1900; $mon += 1;
875 my $due_date = sprintf(
876 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
877 $year, $mon, $mday, $hour, $min, $sec);
881 sub _set_circ_due_date {
884 my $dd = _create_due_date($circ->duration);
885 $logger->debug("Checkout setting due date on circ to: $dd");
886 $circ->due_date($dd);
889 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
890 sub _update_checkout_copy {
893 my $copy = $ctx->{copy};
895 my $s = $U->copy_status_from_name('checked out');
896 $copy->status( $s->id ) if $s;
898 my $evt = $U->update_copy( session => $ctx->{session},
899 copy => $copy, editor => $ctx->{requestor}->id );
900 return (undef,$evt) if $evt;
905 # commits the circ object to the db then fleshes the circ with rules objects
906 sub _commit_checkout_circ_object {
909 my $circ = $ctx->{circ};
913 my $r = $ctx->{session}->request(
914 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
916 return $U->DB_UPDATE_FAILED($circ) unless $r;
918 $logger->debug("Created a new circ object in checkout: $r");
921 $circ->duration_rule($ctx->{duration_rule});
922 $circ->max_fine_rule($ctx->{max_fine_rule});
923 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
929 # sees if there are any holds that this copy
930 sub _handle_related_holds {
933 my $copy = $ctx->{copy};
934 my $patron = $ctx->{patron};
935 my $holds = $holdcode->fetch_related_holds($copy->id);
939 # XXX We should only fulfill one hold here...
940 # XXX If a hold was transited to the user who is checking out
941 # the item, we need to make sure that hold is what's grabbed
942 if(ref($holds) && @$holds) {
944 # for now, just sort by id to get what should be the oldest hold
945 $holds = [ sort { $a->id <=> $b->id } @$holds ];
946 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
949 my $hold = $holds->[0];
951 $logger->debug("Related hold found in checkout: " . $hold->id );
953 $hold->current_copy($copy->id); # just make sure it's set
954 # if the hold was never officially captured, capture it.
955 $hold->capture_time('now') unless $hold->capture_time;
956 $hold->fulfillment_time('now');
957 my $r = $ctx->{session}->request(
958 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
959 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
960 push( @fulfilled, $hold->id );
964 return (\@fulfilled, undef);
967 sub _checkout_noncat {
968 my ( $key, $requestor, $patron, %params ) = @_;
969 my( $circ, $circlib, $evt );
972 $circlib = $params{noncat_circ_lib} || $requestor->home_ou;
974 my $count = $params{noncat_count} || 1;
975 my $cotime = _create_date_stamp($params{checkout_time}) || "";
976 $logger->info("circ creating $count noncat circs with checkout time $cotime");
978 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
979 $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
983 return OpenILS::Event->new(
984 'SUCCESS', payload => { noncat_circ => $circ } );
988 __PACKAGE__->register_method(
989 method => "generic_receive",
990 api_name => "open-ils.circ.checkin",
993 Generic super-method for handling all copies
994 @param authtoken The login session key
995 @param params Hash of named parameters including:
996 barcode - The copy barcode
997 force - If true, copies in bad statuses will be checked in and give good statuses
1002 sub generic_receive {
1003 my( $self, $connection, $authtoken, $params ) = @_;
1004 my( $ctx, $requestor, $evt );
1006 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1007 ( $requestor, $evt ) = $U->checksesperm(
1008 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1009 return $evt if $evt;
1011 # load up the circ objects
1012 if( !( $ctx = $params->{_ctx}) ) {
1013 ( $ctx, $evt ) = create_circ_ctx( %$params,
1014 requestor => $requestor,
1015 session => $U->start_db_session(),
1017 fetch_copy_statuses => 1,
1018 fetch_copy_locations => 1,
1021 return $evt if $evt;
1023 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1024 $ctx->{authtoken} = $authtoken;
1025 my $session = $ctx->{session};
1027 my $copy = $ctx->{copy};
1028 $U->unflesh_copy($copy);
1029 return OpenILS::Event->new('COPY_NOT_FOUND') unless $copy;
1031 $logger->info("Checkin copy called by user ".
1032 $requestor->id." for copy ".$copy->id);
1034 return $self->checkin_do_receive($connection, $ctx);
1037 sub checkin_do_receive {
1039 my( $self, $connection, $ctx ) = @_;
1042 my $copy = $ctx->{copy};
1043 my $session = $ctx->{session};
1044 my $requestor = $ctx->{requestor};
1045 my $change = 0; # did we actually do anything?
1047 if(!$ctx->{force}) {
1048 return $evt if ($evt = _checkin_check_copy_status($copy));
1051 ($ctx->{circ}, $evt) = $U->fetch_open_circulation($copy->id);
1052 return $evt if ($evt and $__isrenewal); # renewals require a circulation
1054 ($ctx->{transit}) = $U->fetch_open_transit_by_copy($copy->id);
1056 if( $ctx->{circ} ) {
1058 # There is an open circ on this item, close it out.
1060 $evt = _checkin_handle_circ($ctx);
1061 return $evt if $evt;
1063 } elsif( $ctx->{transit} ) {
1065 # is this item currently in transit?
1067 $evt = $transcode->transit_receive( $copy, $requestor, $session );
1068 my $holdtrans = $evt->{holdtransit};
1069 ($ctx->{hold}) = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1071 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1073 # either an error occurred or a ROUTE_ITEM was generated and the
1074 # item must be forwarded on to its destination.
1075 return _checkin_flesh_event($ctx, $evt);
1081 # copy was received as a hold transit. Copy is at target lib
1082 # and hold transit is complete. We're done here...
1083 $U->commit_db_session($session);
1084 return _checkin_flesh_event($ctx, $evt);
1090 # ------------------------------------------------------------------------------
1091 # Circulations and transits are now closed where necessary. Now go on to see if
1092 # this copy can fulfill a hold or needs to be routed to a different location
1093 # ------------------------------------------------------------------------------
1096 # If it's a renewal, we're done
1098 $U->commit_db_session($session);
1099 return OpenILS::Event->new('SUCCESS');
1103 # Now, let's see if this copy is needed for a hold
1104 my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor );
1108 $ctx->{hold} = $hold;
1111 # Capture the hold with this copy
1112 return $evt if ($evt = _checkin_capture_hold($ctx));
1114 if( $hold->pickup_lib == $requestor->home_ou ) {
1116 # This hold was captured in the correct location
1117 $evt = OpenILS::Event->new('SUCCESS');
1121 # Hold needs to be picked up elsewhere. Build a hold
1122 # transit and route the item.
1123 return $evt if ($evt =_checkin_build_hold_transit($ctx));
1124 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1127 } else { # not needed for a hold
1129 if( $copy->circ_lib == $requestor->home_ou ) {
1131 # Copy is in the right place.
1132 $evt = OpenILS::Event->new('SUCCESS');
1134 # if the item happens to be a pre-cataloged item, send it
1135 # to cataloging and return the event
1136 my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1137 return $err if $err;
1143 # Copy wants to go home. Transit it there.
1144 return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1145 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1150 $logger->info("Copy checkin finished with event: ".$evt->{textcode});
1154 $evt = OpenILS::Event->new('NO_CHANGE');
1155 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id)
1156 if ($copy->status == $U->copy_status_from_name('on holds shelf')->id);
1160 $U->commit_db_session($session);
1163 return _checkin_flesh_event($ctx, $evt);
1166 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1167 sub _checkin_handle_precat {
1170 my $copy = $ctx->{copy};
1175 my $catstat = $U->copy_status_from_name('cataloging');
1177 if( $ctx->{precat} ) {
1179 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1181 if( $copy->status != $catstat->id ) {
1182 $copy->status($catstat->id);
1184 return (undef, 0, $errevt) if (
1185 $errevt = $U->update_copy(
1187 editor => $ctx->{requestor}->id,
1188 session => $ctx->{session} ));
1194 return ($evt, $change, undef);
1198 sub _checkin_check_copy_status {
1200 my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1201 my $evt = OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1202 return $evt if ($stat == $U->copy_status_from_name('lost')->id);
1203 return $evt if ($stat == $U->copy_status_from_name('missing')->id);
1207 # Just gets the copy back home. Returns undef on success, event on error
1208 sub _checkin_build_generic_copy_transit {
1211 my $requestor = $ctx->{requestor};
1212 my $copy = $ctx->{copy};
1213 my $transit = Fieldmapper::action::transit_copy->new;
1214 my $session = $ctx->{session};
1216 $logger->activity("User ". $requestor->id ." creating a ".
1217 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1219 $transit->source($requestor->home_ou);
1220 $transit->dest($copy->circ_lib);
1221 $transit->target_copy($copy->id);
1222 $transit->source_send_time('now');
1223 $transit->copy_status($copy->status);
1225 $logger->debug("Creating new copy_transit in DB");
1227 my $s = $session->request(
1228 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1229 return $U->DB_UPDATE_FAILED($transit) unless $s;
1231 $logger->info("Checkin copy successfully created new transit: $s");
1233 $copy->status($U->copy_status_from_name('in transit')->id );
1235 return $U->update_copy( copy => $copy,
1236 editor => $requestor->id, session => $session );
1241 # returns event on error, undef on success
1242 sub _checkin_build_hold_transit {
1245 my $copy = $ctx->{copy};
1246 my $hold = $ctx->{hold};
1247 my $trans = Fieldmapper::action::hold_transit_copy->new;
1249 $trans->hold($hold->id);
1250 $trans->source($ctx->{requestor}->home_ou);
1251 $trans->dest($hold->pickup_lib);
1252 $trans->source_send_time("now");
1253 $trans->target_copy($copy->id);
1254 $trans->copy_status($copy->status);
1256 my $id = $ctx->{session}->request(
1257 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1258 return $U->DB_UPDATE_FAILED($trans) unless $id;
1260 $logger->info("Checkin copy successfully created hold transit: $id");
1262 $copy->status($U->copy_status_from_name('in transit')->id );
1263 return $U->update_copy( copy => $copy,
1264 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1267 # Returns event on error, undef on success
1268 sub _checkin_capture_hold {
1270 my $copy = $ctx->{copy};
1271 my $hold = $ctx->{hold};
1273 $logger->debug("Checkin copy capturing hold ".$hold->id);
1275 $hold->current_copy($copy->id);
1276 $hold->capture_time('now');
1278 my $stat = $ctx->{session}->request(
1279 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1280 return $U->DB_UPDATE_FAILED($hold) unless $stat;
1282 $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1284 return $U->update_copy( copy => $copy,
1285 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1288 # fleshes an event with the relevant objects from the context
1289 sub _checkin_flesh_event {
1294 $payload->{copy} = $U->unflesh_copy($ctx->{copy});
1295 $payload->{record} = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1296 $payload->{circ} = $ctx->{circ} if $ctx->{circ};
1297 $payload->{transit} = $ctx->{transit} if $ctx->{transit};
1298 $payload->{hold} = $ctx->{hold} if $ctx->{hold};
1300 $evt->{payload} = $payload;
1305 # Closes out the circulation, puts the copy into reshelving.
1306 # Voids any bills attached to this circ after the backdate time
1307 # if a backdate is provided
1308 sub _checkin_handle_circ {
1312 my $circ = $ctx->{circ};
1313 my $copy = $ctx->{copy};
1314 my $requestor = $ctx->{requestor};
1315 my $session = $ctx->{session};
1317 $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1319 $ctx->{longoverdue} = 1 if ($circ->stop_fines =~ /longoverdue/io);
1320 $ctx->{claimsreturned} = 1 if ($circ->stop_fines =~ /claimsreturned/io);
1322 my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1323 return $evt if $evt;
1325 $circ->stop_fines('CHECKIN');
1326 $circ->stop_fines('RENEW') if $__isrenewal;
1327 $circ->stop_fines('LOST') if($__islost);
1328 $circ->xact_finish('now') if($obt->balance_owed <= 0 and !$__islost);
1329 $circ->stop_fines_time('now');
1330 $circ->checkin_time('now');
1331 $circ->checkin_staff($requestor->id);
1333 if(my $backdate = $ctx->{backdate}) {
1334 return $evt if ($evt =
1335 _checkin_handle_backdate($backdate, $circ, $requestor, $session));
1338 $logger->info("Checkin copy setting status to 'reshelving' and committing...");
1339 $copy->status($U->copy_status_from_name('reshelving')->id);
1340 $evt = $U->update_copy( session => $session,
1341 copy => $copy, editor => $requestor->id );
1342 return $evt if $evt;
1344 $ctx->{session}->request(
1345 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1350 # returns event on error, undef on success
1351 # This voids all bills attached to the given circulation that occurred
1352 # after the backdate
1353 sub _checkin_handle_backdate {
1354 my( $backdate, $circ, $requestor, $session ) = @_;
1356 $logger->activity("User ".$requestor->id.
1357 " backdating circ [".$circ->target_copy."] to date: $backdate");
1359 $circ->xact_finish($backdate);
1361 my $bills = $session->request( # XXX Verify this call is correct
1362 "open-ils.storage.direct.money.billing.search_where.atomic",
1363 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1366 for my $bill (@$bills) {
1368 my $s = $session->request(
1369 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1370 return $U->DB_UPDATE_FAILED($bill) unless $s;
1379 # ------------------------------------------------------------------------------
1381 __PACKAGE__->register_method(
1383 api_name => "open-ils.circ.renew",
1384 notes => <<" NOTES");
1385 PARAMS( authtoken, circ => circ_id );
1386 open-ils.circ.renew(login_session, circ_object);
1387 Renews the provided circulation. login_session is the requestor of the
1388 renewal and if the logged in user is not the same as circ->usr, then
1389 the logged in user must have RENEW_CIRC permissions.
1393 my( $self, $client, $authtoken, $params ) = @_;
1396 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1399 # fetch the patron object one way or another
1400 if( $params->{patron} ) {
1401 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1402 if($evt) { $__isrenewal = 0; return $evt; }
1404 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1405 if($evt) { $__isrenewal = 0; return $evt; }
1408 # verify our login session
1409 ($requestor, $evt) = $U->checkses($authtoken);
1410 if($evt) { $__isrenewal = 0; return $evt; }
1412 # make sure we have permission to perform a renewal
1413 if( $requestor->id ne $patron->id ) {
1414 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'RENEW_CIRC');
1415 if($evt) { $__isrenewal = 0; return $evt; }
1419 # fetch and build the circulation environment
1420 ( $ctx, $evt ) = create_circ_ctx( %$params,
1422 requestor => $requestor,
1425 fetch_patron_circ_summary => 1,
1426 fetch_copy_statuses => 1,
1427 fetch_copy_locations => 1,
1429 if($evt) { $__isrenewal = 0; return $evt; }
1430 $params->{_ctx} = $ctx;
1432 # make sure they have some renewals left and make sure the circulation exists
1433 ($circ, $evt) = _check_renewal_remaining($ctx);
1434 if($evt) { $__isrenewal = 0; return $evt; }
1435 $ctx->{old_circ} = $circ;
1436 my $renewals = $circ->renewal_remaining - 1;
1438 # run the renew permit script
1439 $evt = _run_renew_scripts($ctx);
1440 if($evt) { $__isrenewal = 0; return $evt; }
1443 #$ctx->{patron} = $ctx->{patron}->id;
1444 $evt = $self->generic_receive($client, $authtoken, $ctx );
1445 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1447 if( !$U->event_equals($evt, 'SUCCESS') ) {
1448 $__isrenewal = 0; return $evt;
1451 # re-fetch the context since objects have changed in the checkin
1452 ( $ctx, $evt ) = create_circ_ctx( %$params,
1454 requestor => $requestor,
1457 fetch_patron_circ_summary => 1,
1458 fetch_copy_statuses => 1,
1459 fetch_copy_locations => 1,
1461 if($evt) { $__isrenewal = 0; return $evt; }
1462 $params->{_ctx} = $ctx;
1463 $ctx->{renewal_remaining} = $renewals;
1465 # run the circ permit scripts
1466 if( $ctx->{permit_override} ) {
1467 $evt = $U->check_perms(
1468 $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1469 if($evt) { $__isrenewal = 0; return $evt; }
1472 $evt = $self->permit_circ( $client, $authtoken, $params );
1473 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1477 if(!$U->event_equals($evt, 'SUCCESS')) {
1478 if($evt) { $__isrenewal = 0; return $evt; }
1481 $params->{permit_key} = $evt->{payload};
1485 # checkout the item again
1486 $evt = $self->checkout($client, $authtoken, $params );
1492 sub _check_renewal_remaining {
1495 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1496 return (undef, $evt) if $evt;
1497 $evt = OpenILS::Event->new(
1498 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1499 return ($circ, $evt);
1502 sub _run_renew_scripts {
1504 my $runner = $ctx->{runner};
1507 $runner->load($scripts{circ_permit_renew});
1508 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1509 #my $evtname = $runner->retrieve('result.event');
1510 #$logger->activity("circ_permit_renew for user ".$ctx->{patron}->id." returned event: $evtname");
1512 my $events = $runner->retrieve('result.events');
1513 $events = [ split(/,/, $events) ];
1514 $logger->activity("circ_permit_renew for user ".$ctx->{patron}->id." returned events: @$events");
1517 push( @allevents, OpenILS::Event->new($_)) for @$events;
1518 return \@allevents if @allevents;
1520 #return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';