1 package OpenILS::Application::Circ::Circulate;
2 use base 'OpenSRF::Application';
3 use strict; use warnings;
4 use OpenSRF::EX qw(:try);
7 use OpenSRF::Utils::Cache;
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 $Data::Dumper::Indent = 0;
17 my $apputils = "OpenILS::Application::AppUtils";
19 my $holdcode = "OpenILS::Application::Circ::Holds";
20 my $transcode = "OpenILS::Application::Circ::Transit";
22 my %scripts; # - circulation script filenames
23 my $script_libs; # - any additional script libraries
24 my %cache; # - db objects cache
25 my %contexts; # - Script runner contexts
26 my $cache_handle; # - memcache handle
28 sub PRECAT_FINE_LEVEL { return 2; }
29 sub PRECAT_LOAN_DURATION { return 2; }
32 # for security, this is a process-defined and not
33 # a client-defined variable
37 # ------------------------------------------------------------------------------
38 # Load the circ script from the config
39 # ------------------------------------------------------------------------------
43 $cache_handle = OpenSRF::Utils::Cache->new('global');
44 my $conf = OpenSRF::Utils::SettingsClient->new;
45 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
46 my @pfx = ( @pfx2, "scripts" );
48 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
49 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
50 my $d = $conf->config_value( @pfx, 'circ_duration' );
51 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
52 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
53 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
54 my $lb = $conf->config_value( @pfx2, 'script_path' );
56 $logger->error( "Missing circ script(s)" )
57 unless( $p and $c and $d and $f and $m and $pr );
59 $scripts{circ_permit_patron} = $p;
60 $scripts{circ_permit_copy} = $c;
61 $scripts{circ_duration} = $d;
62 $scripts{circ_recurring_fines}= $f;
63 $scripts{circ_max_fines} = $m;
64 $scripts{circ_permit_renew} = $pr;
66 $lb = [ $lb ] unless ref($lb);
69 $logger->debug("Loaded rules scripts for circ: " .
70 "circ permit patron: $p, circ permit copy: $c, ".
71 "circ duration :$d , circ recurring fines : $f, " .
72 "circ max fines : $m, circ renew permit : $pr");
76 # ------------------------------------------------------------------------------
77 # Loads the necessary circ objects and pushes them into the script environment
78 # Returns ( $data, $evt ). if $evt is defined, then an
79 # unexpedted event occurred and should be dealt with / returned to the caller
80 # ------------------------------------------------------------------------------
88 $evt = _ctx_add_patron_objects($ctx, %params);
89 return (undef,$evt) if $evt;
91 if(!$params{noncat}) {
92 if( $evt = _ctx_add_copy_objects($ctx, %params) ) {
93 $ctx->{precat} = 1 if($evt->{textcode} eq 'COPY_NOT_FOUND')
95 $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
99 _doctor_patron_object($ctx) if $ctx->{patron};
100 _doctor_copy_object($ctx) if $ctx->{copy};
102 if(!$ctx->{no_runner}) {
103 _build_circ_script_runner($ctx);
104 _add_script_runner_methods($ctx);
110 sub _ctx_add_patron_objects {
111 my( $ctx, %params) = @_;
114 if(!defined($cache{patron_standings})) {
115 $cache{patron_standings} = $U->fetch_patron_standings();
116 $cache{group_tree} = $U->fetch_permission_group_tree();
119 $ctx->{patron_standings} = $cache{patron_standings};
120 $ctx->{group_tree} = $cache{group_tree};
122 $ctx->{patron_circ_summary} =
123 $U->fetch_patron_circ_summary($ctx->{patron}->id)
124 if $params{fetch_patron_circsummary};
130 sub _find_copy_by_attr {
135 my $copy = $params{copy} || undef;
140 $U->fetch_copy($params{copyid}) if $params{copyid};
141 return (undef,$evt) if $evt;
145 $U->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
146 return (undef,$evt) if $evt;
149 return ( $copy, $evt );
152 sub _ctx_add_copy_objects {
153 my($ctx, %params) = @_;
158 $cache{copy_statuses} = $U->fetch_copy_statuses
159 if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
161 $cache{copy_locations} = $U->fetch_copy_locations
162 if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
164 $ctx->{copy_statuses} = $cache{copy_statuses};
165 $ctx->{copy_locations} = $cache{copy_locations};
167 ($copy, $evt) = _find_copy_by_attr(%params);
170 if( $copy and !$ctx->{title} ) {
171 $logger->debug("Copy status: " . $copy->status);
172 ( $ctx->{title}, $evt ) = $U->fetch_record_by_copy( $copy->id );
174 $ctx->{copy} = $copy;
181 # ------------------------------------------------------------------------------
182 # Fleshes parts of the patron object
183 # ------------------------------------------------------------------------------
184 sub _doctor_copy_object {
187 my $copy = $ctx->{copy} || return undef;
189 $logger->debug("Doctoring copy object...");
191 # set the copy status to a status name
192 $copy->status( _get_copy_status( $copy, $ctx->{copy_statuses} ) );
194 # set the copy location to the location object
195 $copy->location( _get_copy_location( $copy, $ctx->{copy_locations} ) );
197 $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
201 # ------------------------------------------------------------------------------
202 # Fleshes parts of the patron object
203 # ------------------------------------------------------------------------------
204 sub _doctor_patron_object {
207 my $patron = $ctx->{patron} || return undef;
209 # push the standing object into the patron
210 if(ref($ctx->{patron_standings})) {
211 for my $s (@{$ctx->{patron_standings}}) {
212 if( $s->id eq $ctx->{patron}->standing ) {
213 $patron->standing($s);
214 $logger->debug("Set patron standing to ". $s->value);
219 # set the patron ptofile to the profile name
220 $patron->profile( _get_patron_profile(
221 $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
225 $U->fetch_org_unit( $patron->home_ou ) ) if $patron;
229 # recurse and find the patron profile name from the tree
230 # another option would be to grab the groups for the patron
231 # and cycle through those until the "profile" group has been found
232 sub _get_patron_profile {
233 my( $patron, $group_tree ) = @_;
234 return $group_tree if ($group_tree->id eq $patron->profile);
235 return undef unless ($group_tree->children);
237 for my $child (@{$group_tree->children}) {
238 my $ret = _get_patron_profile( $patron, $child );
244 sub _get_copy_status {
245 my( $copy, $cstatus ) = @_;
248 for my $status (@$cstatus) {
249 $s = $status if( $status->id eq $copy->status )
251 $logger->debug("Retrieving copy status: " . $s->name) if $s;
255 sub _get_copy_location {
256 my( $copy, $locations ) = @_;
259 for my $loc (@$locations) {
260 $l = $loc if $loc->id eq $copy->location;
262 $logger->debug("Retrieving copy location: " . $l->name ) if $l;
267 # ------------------------------------------------------------------------------
268 # Constructs and shoves data into the script environment
269 # ------------------------------------------------------------------------------
270 sub _build_circ_script_runner {
274 $logger->debug("Loading script environment for circulation");
277 if( $runner = $contexts{$ctx->{type}} ) {
278 $runner->refresh_context;
280 $runner = OpenILS::Utils::ScriptRunner->new;
281 $contexts{type} = $runner;
285 $logger->debug("Loading circ script lib path $_");
286 $runner->add_path( $_ );
289 # Note: inserting the number 0 into the script turns into the
290 # string "0", and thus evaluates to true in JS land
291 # inserting undef will insert "", which evaluates to false
293 $runner->insert( 'environment.patron', $ctx->{patron}, 1);
294 $runner->insert( 'environment.title', $ctx->{title}, 1);
295 $runner->insert( 'environment.copy', $ctx->{copy}, 1);
298 $runner->insert( 'result', {} );
299 $runner->insert( 'result.event', 'SUCCESS' );
302 $runner->insert('environment.isRenewal', 1);
304 $runner->insert('environment.isRenewal', undef);
307 if($ctx->{ishold} ) {
308 $runner->insert('environment.isHold', 1);
310 $runner->insert('environment.isHold', undef)
313 if( $ctx->{noncat} ) {
314 $runner->insert('environment.isNonCat', 1);
315 $runner->insert('environment.nonCatType', $ctx->{noncat_type});
317 $runner->insert('environment.isNonCat', undef);
320 if(ref($ctx->{patron_circ_summary})) {
321 $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
322 $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
325 $ctx->{runner} = $runner;
330 sub _add_script_runner_methods {
333 my $runner = $ctx->{runner};
337 # allows a script to fetch a hold that is currently targeting the
339 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
341 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
342 $hold = undef unless $hold;
343 $runner->insert( $key, $hold, 1 );
349 # ------------------------------------------------------------------------------
351 __PACKAGE__->register_method(
352 method => "permit_circ",
353 api_name => "open-ils.circ.checkout.permit",
355 Determines if the given checkout can occur
356 @param authtoken The login session key
357 @param params A trailing hash of named params including
358 barcode : The copy barcode,
359 patron : The patron the checkout is occurring for,
360 renew : true or false - whether or not this is a renewal
361 @return The event that occurred during the permit check.
365 my( $self, $client, $authtoken, $params ) = @_;
368 my ( $requestor, $patron, $ctx, $evt, $circ );
370 # check permisson of the requestor
371 ( $requestor, $patron, $evt ) =
372 $U->checkses_requestor(
373 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
376 # fetch and build the circulation environment
377 if( !( $ctx = $params->{_ctx}) ) {
379 ( $ctx, $evt ) = create_circ_ctx( %$params,
381 requestor => $requestor,
383 fetch_patron_circ_summary => 1,
384 fetch_copy_statuses => 1,
385 fetch_copy_locations => 1,
390 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
391 ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
392 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
395 return _run_permit_scripts($ctx);
399 __PACKAGE__->register_method(
400 method => "check_title_hold",
401 api_name => "open-ils.circ.title_hold.is_possible",
403 Determines if a hold were to be placed by a given user,
404 whether or not said hold would have any potential copies
406 @param authtoken The login session key
407 @param params A hash of named params including:
408 patronid - the id of the hold recipient
409 titleid (brn) - the id of the title to be held
410 depth - the hold range depth (defaults to 0)
413 sub check_title_hold {
414 my( $self, $client, $authtoken, $params ) = @_;
415 my %params = %$params;
416 my $titleid = $params{titleid};
418 my ( $requestor, $patron, $evt ) = $U->checkses_requestor(
419 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
422 my $rangelib = $patron->home_ou;
423 my $depth = $params{depth} || 0;
425 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
427 my $title = $U->storagereq(
428 'open-ils.storage.biblio.record_entry.ranged_tree', $titleid, $rangelib );
430 my $org = $U->simplereq(
432 'open-ils.actor.org_unit.retrieve',
433 $authtoken, $requestor->home_ou );
435 for my $cn (@{$title->call_numbers}) {
437 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
439 for my $copy (@{$cn->copies}) {
441 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
443 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
445 requestor => $requestor,
448 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
449 request_lib => $org } );
451 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
460 # Runs the patron and copy permit scripts
461 # if this is a non-cat circulation, the copy permit script
463 sub _run_permit_scripts {
465 my $runner = $ctx->{runner};
466 my $patronid = $ctx->{patron}->id;
467 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
470 $runner->load($scripts{circ_permit_patron});
471 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
472 my $evtname = $runner->retrieve('result.event');
473 $logger->activity("circ_permit_patron for user $patronid returned event: $evtname");
475 return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';
477 my $key = _cache_permit_key();
479 if( $ctx->{noncat} ) {
480 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
481 return OpenILS::Event->new('SUCCESS', payload => $key);
485 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
486 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
490 $logger->debug("Exiting circ permit early because request is for hold patron permit");
491 return OpenILS::Event->new('SUCCESS');
494 $runner->load($scripts{circ_permit_copy});
495 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
496 $evtname = $runner->retrieve('result.event');
497 $logger->activity("circ_permit_copy for user $patronid ".
498 "and copy $barcode returned event: $evtname");
500 return OpenILS::Event->new($evtname, payload => $key) if( $evtname eq 'SUCCESS' );
501 return OpenILS::Event->new($evtname);
504 # takes copyid, patronid, and requestor id
505 sub _cache_permit_key {
506 my $key = md5_hex( time() . rand() . "$$" );
507 $logger->debug("Setting circ permit key to $key");
508 $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
512 sub _check_permit_key {
514 $logger->debug("Fetching circ permit key $key");
515 my $k = "oils_permit_key_$key";
516 my $one = $cache_handle->get_cache($k);
517 $cache_handle->delete_cache($k);
518 return ($one) ? 1 : 0;
522 # ------------------------------------------------------------------------------
524 __PACKAGE__->register_method(
525 method => "checkout",
526 api_name => "open-ils.circ.checkout",
529 @param authtoken The login session key
530 @param params A named hash of params including:
532 barcode If no copy is provided, the copy is retrieved via barcode
533 copyid If no copy or barcode is provide, the copy id will be use
534 patron The patron's id
535 noncat True if this is a circulation for a non-cataloted item
536 noncat_type The non-cataloged type id
537 noncat_circ_lib The location for the noncat circ.
538 precat The item has yet to be cataloged
539 dummy_title The temporary title of the pre-cataloded item
540 dummy_author The temporary authr of the pre-cataloded item
541 Default is the home org of the staff member
542 @return The SUCCESS event on success, any other event depending on the error
546 my( $self, $client, $authtoken, $params ) = @_;
549 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
550 my $key = $params->{permit_key};
552 # if this is a renewal, then the requestor does not have to
553 # have checkout privelages
554 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
555 ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
557 $logger->debug("REQUESTOR event: " . ref($requestor));
560 ( $patron, $evt ) = $U->fetch_user($params->{patron});
564 # set the circ lib to the home org of the requestor if not specified
565 my $circlib = (defined($params->{circ_lib})) ?
566 $params->{circ_lib} : $requestor->home_ou;
568 # if this is a non-cataloged item, check it out and return
569 return _checkout_noncat(
570 $key, $requestor, $patron, %$params ) if $params->{noncat};
572 # if this item has yet to be cataloged, make sure a dummy copy exists
573 ( $params->{copy}, $evt ) = _make_precat_copy(
574 $requestor, $circlib, $params ) if $params->{precat};
577 # fetch and build the circulation environment
578 if( !( $ctx = $params->{_ctx}) ) {
579 ( $ctx, $evt ) = create_circ_ctx( %$params,
581 requestor => $requestor,
582 session => $U->start_db_session(),
584 fetch_patron_circ_summary => 1,
585 fetch_copy_statuses => 1,
586 fetch_copy_locations => 1,
590 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
592 my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
593 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
594 unless _check_permit_key($key);
596 $ctx->{circ_lib} = $circlib;
598 $evt = _run_checkout_scripts($ctx);
601 _build_checkout_circ_object($ctx);
603 $evt = _commit_checkout_circ_object($ctx);
606 $evt = _update_checkout_copy($ctx);
610 ($holds, $evt) = _handle_related_holds($ctx);
614 $logger->debug("Checkin committing objects with session thread trace: ".$ctx->{session}->session_id);
615 $U->commit_db_session($ctx->{session});
616 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
618 return OpenILS::Event->new('SUCCESS',
620 copy => $U->unflesh_copy($ctx->{copy}),
621 circ => $ctx->{circ},
623 holds_fulfilled => $holds,
628 sub _make_precat_copy {
629 my ( $requestor, $circlib, $params ) = @_;
631 my( $copy, undef ) = _find_copy_by_attr(%$params);
634 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
635 return ($copy, undef);
638 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
640 my $evt = OpenILS::Event->new(
641 'BAD_PARAMS', desc => "Dummy title or author not provided" )
642 unless ( $params->{dummy_title} and $params->{dummy_author} );
643 return (undef, $evt) if $evt;
645 $copy = Fieldmapper::asset::copy->new;
646 $copy->circ_lib($circlib);
647 $copy->creator($requestor->id);
648 $copy->editor($requestor->id);
649 $copy->barcode($params->{barcode});
650 $copy->call_number(-1); #special CN for precat materials
651 $copy->loan_duration(&PRECAT_LOAN_DURATION); # these two should come from constants
652 $copy->fine_level(&PRECAT_FINE_LEVEL);
654 $copy->dummy_title($params->{dummy_title});
655 $copy->dummy_author($params->{dummy_author});
657 my $id = $U->storagereq(
658 'open-ils.storage.direct.asset.copy.create', $copy );
659 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
661 $logger->debug("Pre-cataloged copy successfully created");
662 return $U->fetch_copy($id);
666 sub _run_checkout_scripts {
672 my $runner = $ctx->{runner};
674 $runner->insert('result.durationLevel');
675 $runner->insert('result.durationRule');
676 $runner->insert('result.recurringFinesRule');
677 $runner->insert('result.recurringFinesLevel');
678 $runner->insert('result.maxFine');
680 $runner->load($scripts{circ_duration});
681 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
682 my $duration = $runner->retrieve('result.durationRule');
683 $logger->debug("Circ duration script yielded a duration rule of: $duration");
685 $runner->load($scripts{circ_recurring_fines});
686 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
687 my $recurring = $runner->retrieve('result.recurringFinesRule');
688 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
690 $runner->load($scripts{circ_max_fines});
691 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
692 my $max_fine = $runner->retrieve('result.maxFine');
693 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
695 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
697 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
699 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
702 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
703 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
704 $ctx->{duration_rule} = $duration;
705 $ctx->{recurring_fines_rule} = $recurring;
706 $ctx->{max_fine_rule} = $max_fine;
711 sub _build_checkout_circ_object {
715 my $circ = new Fieldmapper::action::circulation;
716 my $duration = $ctx->{duration_rule};
717 my $max = $ctx->{max_fine_rule};
718 my $recurring = $ctx->{recurring_fines_rule};
719 my $copy = $ctx->{copy};
720 my $patron = $ctx->{patron};
721 my $dur_level = $ctx->{duration_level};
722 my $rec_level = $ctx->{recurring_fines_level};
724 $circ->duration( $duration->shrt ) if ($dur_level == 1);
725 $circ->duration( $duration->normal ) if ($dur_level == 2);
726 $circ->duration( $duration->extended ) if ($dur_level == 3);
728 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
729 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
730 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
732 $circ->duration_rule( $duration->name );
733 $circ->recuring_fine_rule( $recurring->name );
734 $circ->max_fine_rule( $max->name );
735 $circ->max_fine( $max->amount );
737 $circ->fine_interval($recurring->recurance_interval);
738 $circ->renewal_remaining( $duration->max_renewals );
739 $circ->target_copy( $copy->id );
740 $circ->usr( $patron->id );
741 $circ->circ_lib( $ctx->{circ_lib} );
744 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
745 $circ->opac_renewal(1);
746 $circ->renewal_remaining($ctx->{renewal_remaining});
747 $circ->circ_staff($ctx->{requestor}->id);
750 # if a patron is renewing, 'requestor' will be the patron
751 $circ->circ_staff( $ctx->{requestor}->id );
752 _set_circ_due_date($circ);
753 $ctx->{circ} = $circ;
756 sub _create_due_date {
757 my $duration = shift;
760 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
761 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
763 $year += 1900; $mon += 1;
764 my $due_date = sprintf(
765 '%s-%0.2d-%0.2dT%s:%0.2d:%0.s2-00',
766 $year, $mon, $mday, $hour, $min, $sec);
770 sub _set_circ_due_date {
773 my $dd = _create_due_date($circ->duration);
774 $logger->debug("Checkout setting due date on circ to: $dd");
775 $circ->due_date($dd);
778 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
779 sub _update_checkout_copy {
782 my $copy = $ctx->{copy};
784 my $s = $U->copy_status_from_name('checked out');
785 $copy->status( $s->id ) if $s;
787 my $evt = $U->update_copy( session => $ctx->{session},
788 copy => $copy, editor => $ctx->{requestor}->id );
789 return (undef,$evt) if $evt;
794 # commits the circ object to the db then fleshes the circ with rules objects
795 sub _commit_checkout_circ_object {
798 my $circ = $ctx->{circ};
802 my $r = $ctx->{session}->request(
803 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
805 return $U->DB_UPDATE_FAILED($circ) unless $r;
807 $logger->debug("Created a new circ object in checkout: $r");
810 $circ->duration_rule($ctx->{duration_rule});
811 $circ->max_fine_rule($ctx->{max_fine_rule});
812 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
818 # sees if there are any holds that this copy
819 sub _handle_related_holds {
822 my $copy = $ctx->{copy};
823 my $patron = $ctx->{patron};
824 my $holds = $holdcode->fetch_related_holds($copy->id);
828 # XXX should we fulfill all the holds or just the first
829 if(ref($holds) && @$holds) {
831 # for now, just sort by id to get what should be the oldest hold
832 $holds = [ sort { $a->id <=> $b->id } @$holds ];
833 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
836 my $hold = $holds->[0];
838 $logger->debug("Related hold found in checkout: " . $hold->id );
840 # if the hold was never officially captured, capture it.
841 $hold->capture_time('now') unless $hold->capture_time;
842 $hold->fulfillment_time('now');
843 my $r = $ctx->{session}->request(
844 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
845 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
846 push( @fulfilled, $hold->id );
850 return (\@fulfilled, undef);
854 sub _checkout_noncat {
855 my ( $key, $requestor, $patron, %params ) = @_;
856 my( $circ, $circlib, $evt );
859 $circlib = $params{noncat_circ_lib} || $requestor->home_ou;
861 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
862 unless _check_permit_key($key);
864 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
865 $requestor->id, $patron->id, $circlib, $params{noncat_type} );
868 return OpenILS::Event->new(
869 'SUCCESS', payload => { noncat_circ => $circ } );
873 # ------------------------------------------------------------------------------
875 __PACKAGE__->register_method(
877 api_name => "open-ils.circ.checkin",
878 notes => <<" NOTES");
879 PARAMS( authtoken, barcode => bc )
880 Checks in based on barcode
881 Returns an event object whose payload contains the record, circ, and copy
882 If the item needs to be routed, the event is a ROUTE_ITEM event
883 with an additional 'route_to' variable set on the event
887 my( $self, $client, $authtoken, $params ) = @_;
890 my( $ctx, $requestor, $evt, $circ, $copy, $payload, $transit );
892 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
893 ( $requestor, $evt ) = $U->checksesperm(
894 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
897 if( !( $ctx = $params->{_ctx}) ) {
898 ( $ctx, $evt ) = create_circ_ctx( %$params,
899 requestor => $requestor,
900 session => $U->start_db_session(),
902 #fetch_patron_circ_summary => 1,
903 fetch_copy_statuses => 1,
904 fetch_copy_locations => 1,
909 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
910 $ctx->{authtoken} = $authtoken;
912 $copy = $ctx->{copy};
913 return OpenILS::Event->new('COPY_NOT_FOUND') unless $copy;
915 $payload = { copy => $copy };
917 $U->record_to_mvr($ctx->{title})
918 if($ctx->{title} and !$ctx->{precat});
920 # if( $copy->status ==
921 # $U->copy_status_from_name($cache{copy_statuses}, 'lost')->id) {
923 # } else { $__islost = 0; }
925 my $status = $U->copy_status_from_name('in transit');
926 if( $copy->status == $status->id ) {
928 # if this copy is in transit, send it to transit_receive.
929 $evt = $transcode->transit_receive( $copy, $requestor, $ctx->{session} );
930 if( !$U->event_equals($evt, 'SUCCESS')) {
931 $evt->{payload}->{copy} = $U->unflesh_copy($copy);
937 # set the status to available for now for ease of debugging
938 $copy->status( $U->copy_status_from_name('available')->id );
940 # set the status to reshelving XXX needs to fall back to
941 # 'available' after a configurable amount of time
942 #$copy->status( $U->copy_status_from_name('reshelving')->id );
944 # grab the open circ attached to this copy
945 ( $circ, $evt ) = $U->fetch_open_circulation($copy->id);
947 $evt->{payload} = $payload;
948 $evt->{payload}->{copy} = $U->unflesh_copy($copy);
952 $ctx->{circ} = $circ;
953 $payload->{circ} = $circ;
955 # update the circ and copy in the db
956 return $evt if($evt = _update_checkin_circ_and_copy($ctx));
958 # ------------------------------------------------------------------------------
959 # If we get to this point, then the checkin will succeed. We just need to
960 # see if there is any other processing required on this copy
961 # ------------------------------------------------------------------------------
964 if( !($evt = _check_checkin_holds($ctx)) ) {
965 # if no hold is found for the copy, see if it needs to be transited
966 ($evt, $transit) = $self->check_copy_transit($ctx);
967 return $evt if ($evt and !$transit);
968 $payload->{transit} = $transit if $transit;
972 $logger->debug("Checkin succeeded. Committing objects...");
973 $U->commit_db_session($ctx->{session});
975 # if the item is not cataloged and no superceding
976 # events exist, return the proper event
977 if ( $copy->call_number == -1 and !$evt ) {
978 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED') }
980 $evt = OpenILS::Event->new('SUCCESS') if (!$evt or $__isrenewal);
981 $evt->{payload} = $payload;
983 $logger->info("Checkin of copy ".$copy->id." returned event: ".$evt->{textcode});
985 $evt->{payload}->{copy} = $U->unflesh_copy($copy);
990 # returns (undef) if no transit is needed
991 # returns (ROUTE_ITEM, $transit) on succsessful transit creation
992 # return (other event) on failure
993 sub check_copy_transit {
994 my( $self, $ctx ) = @_;
995 my $copy = $ctx->{copy};
997 return (undef) if( $copy->circ_lib == $ctx->{requestor}->home_ou );
999 my ($evt) = $self->method_lookup(
1000 'open-ils.circ.copy_transit.create')->run(
1002 { session => $ctx->{session}, copyid => $copy->id } );
1004 return ($evt, undef) unless $U->event_equals($evt,'SUCCESS');
1006 my $transit = $evt->{payload}->{transit};
1007 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib );
1008 return ($evt, $transit);
1011 sub _check_checkin_holds {
1014 my $session = $ctx->{session};
1015 my $requestor = $ctx->{requestor};
1016 my $copy = $ctx->{copy};
1018 $logger->debug("Searching for a local hold on a copy: " . $session->session_id);
1020 my ( $hold, $evt ) =
1021 $holdcode->find_local_hold( $session, $copy, $requestor );
1024 $evt = OpenILS::Event->new(
1025 'COPY_NEEDED_FOR_HOLD', org => $hold->pickup_lib);
1030 sub _update_checkin_circ_and_copy {
1034 my $circ = $ctx->{circ};
1035 my $copy = $ctx->{copy};
1036 my $requestor = $ctx->{requestor};
1037 my $session = $ctx->{session};
1039 my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1040 return $evt if $evt;
1042 $circ->stop_fines('CHECKIN');
1043 $circ->stop_fines('RENEW') if $__isrenewal;
1044 $circ->stop_fines('LOST') if($__islost);
1045 $circ->xact_finish('now') if($obt->balance_owed <= 0 and !$__islost);
1046 $circ->stop_fines_time('now');
1047 $circ->checkin_time('now');
1048 $circ->checkin_staff($requestor->id);
1050 # if the requestor set a backdate, void all the bills after
1052 if(my $backdate = $ctx->{backdate}) {
1054 $logger->activity("User ".$requestor->id.
1055 " backdating checkin copy [".$ctx->{barcode}."] to date: $backdate");
1057 $circ->xact_finish($backdate);
1059 my $bills = $session->request( # XXX what other search criteria??
1060 "open-ils.storage.direct.money.billing.search_where.atomic",
1061 billing_ts => { ">=" => $backdate })->gather(1);
1064 for my $bill (@$bills) {
1066 my $s = $session->request(
1067 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1068 return $U->DB_UPDATE_FAILED($bill) unless $s;
1073 $logger->debug("Checkin committing copy and circ objects");
1074 $evt = $U->update_copy( session => $session,
1075 copy => $copy, editor => $requestor->id );
1076 return $evt if $evt;
1078 $ctx->{session}->request(
1079 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1086 # ------------------------------------------------------------------------------
1088 __PACKAGE__->register_method(
1090 api_name => "open-ils.circ.renew",
1091 notes => <<" NOTES");
1092 PARAMS( authtoken, circ => circ_id );
1093 open-ils.circ.renew(login_session, circ_object);
1094 Renews the provided circulation. login_session is the requestor of the
1095 renewal and if the logged in user is not the same as circ->usr, then
1096 the logged in user must have RENEW_CIRC permissions.
1100 my( $self, $client, $authtoken, $params ) = @_;
1103 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1106 # if requesting a renewal for someone else, you must have
1108 ( $requestor, $patron, $evt ) = $U->checkses_requestor(
1109 $authtoken, $params->{patron}, 'RENEW_CIRC' );
1110 return $evt if $evt;
1113 # fetch and build the circulation environment
1114 ( $ctx, $evt ) = create_circ_ctx( %$params,
1116 requestor => $requestor,
1119 fetch_patron_circ_summary => 1,
1120 fetch_copy_statuses => 1,
1121 fetch_copy_locations => 1,
1123 return $evt if $evt;
1124 $params->{_ctx} = $ctx;
1126 # make sure they have some renewals left and make sure the circulation exists
1127 ($circ, $evt) = _check_renewal_remaining($ctx);
1128 return $evt if $evt;
1129 $ctx->{old_circ} = $circ;
1130 my $renewals = $circ->renewal_remaining - 1;
1132 # run the renew permit script
1133 return $evt if( ($evt = _run_renew_scripts($ctx)) );
1136 #$ctx->{patron} = $ctx->{patron}->id;
1137 $evt = $self->checkin($client, $authtoken, $ctx );
1138 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1140 return $evt unless $U->event_equals($evt, 'SUCCESS');
1142 # re-fetch the context since objects have changed in the checkin
1143 ( $ctx, $evt ) = create_circ_ctx( %$params,
1145 requestor => $requestor,
1148 fetch_patron_circ_summary => 1,
1149 fetch_copy_statuses => 1,
1150 fetch_copy_locations => 1,
1152 return $evt if $evt;
1153 $params->{_ctx} = $ctx;
1154 $ctx->{renewal_remaining} = $renewals;
1156 # run the circ permit scripts
1157 $evt = $self->permit_circ( $client, $authtoken, $params );
1158 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1161 return $evt unless $U->event_equals($evt, 'SUCCESS');
1163 $params->{permit_key} = $evt->{payload};
1166 # checkout the item again
1167 $evt = $self->checkout($client, $authtoken, $params );
1173 sub _check_renewal_remaining {
1176 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1177 return (undef, $evt) if $evt;
1178 $evt = OpenILS::Event->new(
1179 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1180 return ($circ, $evt);
1183 sub _run_renew_scripts {
1185 my $runner = $ctx->{runner};
1188 $runner->load($scripts{circ_permit_renew});
1189 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1190 my $evtname = $runner->retrieve('result.event');
1191 $logger->activity("circ_permit_renew for user ".$ctx->{patron}->id." returned event: $evtname");
1193 return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';