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");
428 my $org = $U->simplereq(
430 'open-ils.actor.org_unit.retrieve',
431 $authtoken, $requestor->home_ou );
437 while( $title = $U->storagereq(
438 'open-ils.storage.biblio.record_entry.ranged_tree',
439 $titleid, $rangelib, $depth, $limit, $offset ) ) {
441 last unless ref($title);
443 for my $cn (@{$title->call_numbers}) {
445 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
447 for my $copy (@{$cn->copies}) {
449 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
451 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
453 requestor => $requestor,
456 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
457 request_lib => $org } );
459 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
471 # Runs the patron and copy permit scripts
472 # if this is a non-cat circulation, the copy permit script
474 sub _run_permit_scripts {
476 my $runner = $ctx->{runner};
477 my $patronid = $ctx->{patron}->id;
478 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
481 $runner->load($scripts{circ_permit_patron});
482 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
483 my $evtname = $runner->retrieve('result.event');
484 $logger->activity("circ_permit_patron for user $patronid returned event: $evtname");
486 return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';
488 my $key = _cache_permit_key();
490 if( $ctx->{noncat} ) {
491 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
492 return OpenILS::Event->new('SUCCESS', payload => $key);
496 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
497 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
501 $logger->debug("Exiting circ permit early because request is for hold patron permit");
502 return OpenILS::Event->new('SUCCESS');
505 $runner->load($scripts{circ_permit_copy});
506 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
507 $evtname = $runner->retrieve('result.event');
508 $logger->activity("circ_permit_copy for user $patronid ".
509 "and copy $barcode returned event: $evtname");
511 return OpenILS::Event->new($evtname, payload => $key) if( $evtname eq 'SUCCESS' );
512 return OpenILS::Event->new($evtname);
515 # takes copyid, patronid, and requestor id
516 sub _cache_permit_key {
517 my $key = md5_hex( time() . rand() . "$$" );
518 $logger->debug("Setting circ permit key to $key");
519 $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
523 sub _check_permit_key {
525 $logger->debug("Fetching circ permit key $key");
526 my $k = "oils_permit_key_$key";
527 my $one = $cache_handle->get_cache($k);
528 $cache_handle->delete_cache($k);
529 return ($one) ? 1 : 0;
533 # ------------------------------------------------------------------------------
535 __PACKAGE__->register_method(
536 method => "checkout",
537 api_name => "open-ils.circ.checkout",
540 @param authtoken The login session key
541 @param params A named hash of params including:
543 barcode If no copy is provided, the copy is retrieved via barcode
544 copyid If no copy or barcode is provide, the copy id will be use
545 patron The patron's id
546 noncat True if this is a circulation for a non-cataloted item
547 noncat_type The non-cataloged type id
548 noncat_circ_lib The location for the noncat circ.
549 precat The item has yet to be cataloged
550 dummy_title The temporary title of the pre-cataloded item
551 dummy_author The temporary authr of the pre-cataloded item
552 Default is the home org of the staff member
553 @return The SUCCESS event on success, any other event depending on the error
557 my( $self, $client, $authtoken, $params ) = @_;
560 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
561 my $key = $params->{permit_key};
563 # if this is a renewal, then the requestor does not have to
564 # have checkout privelages
565 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
566 ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
568 $logger->debug("REQUESTOR event: " . ref($requestor));
571 ( $patron, $evt ) = $U->fetch_user($params->{patron});
575 # set the circ lib to the home org of the requestor if not specified
576 my $circlib = (defined($params->{circ_lib})) ?
577 $params->{circ_lib} : $requestor->home_ou;
579 # if this is a non-cataloged item, check it out and return
580 return _checkout_noncat(
581 $key, $requestor, $patron, %$params ) if $params->{noncat};
583 # if this item has yet to be cataloged, make sure a dummy copy exists
584 ( $params->{copy}, $evt ) = _make_precat_copy(
585 $requestor, $circlib, $params ) if $params->{precat};
588 # fetch and build the circulation environment
589 if( !( $ctx = $params->{_ctx}) ) {
590 ( $ctx, $evt ) = create_circ_ctx( %$params,
592 requestor => $requestor,
593 session => $U->start_db_session(),
595 fetch_patron_circ_summary => 1,
596 fetch_copy_statuses => 1,
597 fetch_copy_locations => 1,
601 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
603 my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
604 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
605 unless _check_permit_key($key);
607 $ctx->{circ_lib} = $circlib;
609 $evt = _run_checkout_scripts($ctx);
612 _build_checkout_circ_object($ctx);
614 $evt = _commit_checkout_circ_object($ctx);
617 $evt = _update_checkout_copy($ctx);
621 ($holds, $evt) = _handle_related_holds($ctx);
625 $logger->debug("Checkin committing objects with session thread trace: ".$ctx->{session}->session_id);
626 $U->commit_db_session($ctx->{session});
627 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
629 return OpenILS::Event->new('SUCCESS',
631 copy => $U->unflesh_copy($ctx->{copy}),
632 circ => $ctx->{circ},
634 holds_fulfilled => $holds,
639 sub _make_precat_copy {
640 my ( $requestor, $circlib, $params ) = @_;
642 my( $copy, undef ) = _find_copy_by_attr(%$params);
645 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
646 return ($copy, undef);
649 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
651 my $evt = OpenILS::Event->new(
652 'BAD_PARAMS', desc => "Dummy title or author not provided" )
653 unless ( $params->{dummy_title} and $params->{dummy_author} );
654 return (undef, $evt) if $evt;
656 $copy = Fieldmapper::asset::copy->new;
657 $copy->circ_lib($circlib);
658 $copy->creator($requestor->id);
659 $copy->editor($requestor->id);
660 $copy->barcode($params->{barcode});
661 $copy->call_number(-1); #special CN for precat materials
662 $copy->loan_duration(&PRECAT_LOAN_DURATION); # these two should come from constants
663 $copy->fine_level(&PRECAT_FINE_LEVEL);
665 $copy->dummy_title($params->{dummy_title});
666 $copy->dummy_author($params->{dummy_author});
668 my $id = $U->storagereq(
669 'open-ils.storage.direct.asset.copy.create', $copy );
670 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
672 $logger->debug("Pre-cataloged copy successfully created");
673 return $U->fetch_copy($id);
677 sub _run_checkout_scripts {
683 my $runner = $ctx->{runner};
685 $runner->insert('result.durationLevel');
686 $runner->insert('result.durationRule');
687 $runner->insert('result.recurringFinesRule');
688 $runner->insert('result.recurringFinesLevel');
689 $runner->insert('result.maxFine');
691 $runner->load($scripts{circ_duration});
692 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
693 my $duration = $runner->retrieve('result.durationRule');
694 $logger->debug("Circ duration script yielded a duration rule of: $duration");
696 $runner->load($scripts{circ_recurring_fines});
697 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
698 my $recurring = $runner->retrieve('result.recurringFinesRule');
699 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
701 $runner->load($scripts{circ_max_fines});
702 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
703 my $max_fine = $runner->retrieve('result.maxFine');
704 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
706 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
708 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
710 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
713 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
714 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
715 $ctx->{duration_rule} = $duration;
716 $ctx->{recurring_fines_rule} = $recurring;
717 $ctx->{max_fine_rule} = $max_fine;
722 sub _build_checkout_circ_object {
726 my $circ = new Fieldmapper::action::circulation;
727 my $duration = $ctx->{duration_rule};
728 my $max = $ctx->{max_fine_rule};
729 my $recurring = $ctx->{recurring_fines_rule};
730 my $copy = $ctx->{copy};
731 my $patron = $ctx->{patron};
732 my $dur_level = $ctx->{duration_level};
733 my $rec_level = $ctx->{recurring_fines_level};
735 $circ->duration( $duration->shrt ) if ($dur_level == 1);
736 $circ->duration( $duration->normal ) if ($dur_level == 2);
737 $circ->duration( $duration->extended ) if ($dur_level == 3);
739 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
740 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
741 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
743 $circ->duration_rule( $duration->name );
744 $circ->recuring_fine_rule( $recurring->name );
745 $circ->max_fine_rule( $max->name );
746 $circ->max_fine( $max->amount );
748 $circ->fine_interval($recurring->recurance_interval);
749 $circ->renewal_remaining( $duration->max_renewals );
750 $circ->target_copy( $copy->id );
751 $circ->usr( $patron->id );
752 $circ->circ_lib( $ctx->{circ_lib} );
755 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
756 $circ->opac_renewal(1);
757 $circ->renewal_remaining($ctx->{renewal_remaining});
758 $circ->circ_staff($ctx->{requestor}->id);
761 # if a patron is renewing, 'requestor' will be the patron
762 $circ->circ_staff( $ctx->{requestor}->id );
763 _set_circ_due_date($circ);
764 $ctx->{circ} = $circ;
767 sub _create_due_date {
768 my $duration = shift;
771 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
772 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
774 $year += 1900; $mon += 1;
775 my $due_date = sprintf(
776 '%s-%0.2d-%0.2dT%s:%0.2d:%0.s2-00',
777 $year, $mon, $mday, $hour, $min, $sec);
781 sub _set_circ_due_date {
784 my $dd = _create_due_date($circ->duration);
785 $logger->debug("Checkout setting due date on circ to: $dd");
786 $circ->due_date($dd);
789 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
790 sub _update_checkout_copy {
793 my $copy = $ctx->{copy};
795 my $s = $U->copy_status_from_name('checked out');
796 $copy->status( $s->id ) if $s;
798 my $evt = $U->update_copy( session => $ctx->{session},
799 copy => $copy, editor => $ctx->{requestor}->id );
800 return (undef,$evt) if $evt;
805 # commits the circ object to the db then fleshes the circ with rules objects
806 sub _commit_checkout_circ_object {
809 my $circ = $ctx->{circ};
813 my $r = $ctx->{session}->request(
814 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
816 return $U->DB_UPDATE_FAILED($circ) unless $r;
818 $logger->debug("Created a new circ object in checkout: $r");
821 $circ->duration_rule($ctx->{duration_rule});
822 $circ->max_fine_rule($ctx->{max_fine_rule});
823 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
829 # sees if there are any holds that this copy
830 sub _handle_related_holds {
833 my $copy = $ctx->{copy};
834 my $patron = $ctx->{patron};
835 my $holds = $holdcode->fetch_related_holds($copy->id);
839 # XXX should we fulfill all the holds or just the first
840 if(ref($holds) && @$holds) {
842 # for now, just sort by id to get what should be the oldest hold
843 $holds = [ sort { $a->id <=> $b->id } @$holds ];
844 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
847 my $hold = $holds->[0];
849 $logger->debug("Related hold found in checkout: " . $hold->id );
851 # if the hold was never officially captured, capture it.
852 $hold->capture_time('now') unless $hold->capture_time;
853 $hold->fulfillment_time('now');
854 my $r = $ctx->{session}->request(
855 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
856 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
857 push( @fulfilled, $hold->id );
861 return (\@fulfilled, undef);
865 sub _checkout_noncat {
866 my ( $key, $requestor, $patron, %params ) = @_;
867 my( $circ, $circlib, $evt );
870 $circlib = $params{noncat_circ_lib} || $requestor->home_ou;
872 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
873 unless _check_permit_key($key);
875 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
876 $requestor->id, $patron->id, $circlib, $params{noncat_type} );
879 return OpenILS::Event->new(
880 'SUCCESS', payload => { noncat_circ => $circ } );
884 __PACKAGE__->register_method(
885 method => "generic_receive",
886 api_name => "open-ils.circ.checkin",
889 sub generic_receive {
890 my( $self, $connection, $authtoken, $params ) = @_;
891 my( $ctx, $requestor, $evt );
893 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
894 ( $requestor, $evt ) = $U->checksesperm(
895 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
898 # ------------------------------------------------------------------------------
899 # load up the circ objects
900 if( !( $ctx = $params->{_ctx}) ) {
901 ( $ctx, $evt ) = create_circ_ctx( %$params,
902 requestor => $requestor,
903 session => $U->start_db_session(),
905 fetch_copy_statuses => 1,
906 fetch_copy_locations => 1,
911 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
912 $ctx->{authtoken} = $authtoken;
913 my $session = $ctx->{session};
915 my $copy = $ctx->{copy};
916 $U->unflesh_copy($copy);
917 return OpenILS::Event->new('COPY_NOT_FOUND') unless $copy;
918 $logger->info("Checkin copy called by user ".$requestor->id." for copy ".$copy->id);
919 # ------------------------------------------------------------------------------
921 return $self->checkin_do_receive($connection, $ctx);
924 sub checkin_do_receive {
926 my( $self, $connection, $ctx ) = @_;
929 my $copy = $ctx->{copy};
930 my $session = $ctx->{session};
931 my $requestor = $ctx->{requestor};
932 my $nochange = 1; # did we actually do anything?
935 return $evt if ($evt = _checkin_check_copy_status($copy));
938 my $longoverdue = 0; # is this copy attached to a longoverdue circ?
939 my $claimsret = 0; # is this copy attached to a claims returned circ?
940 my ($circ) = $U->fetch_open_circulation($copy->id);
941 my ($transit) = $U->fetch_open_transit_by_copy($copy->id);
945 # There is an open circ on this item. Grab any interesting
946 # info from the circ then close it out
947 $longoverdue = 1 if ($circ->stop_fines =~ /longoverdue/);
948 $claimsret = 1 if ($circ->stop_fines =~ /claimsreturned/);
950 $ctx->{circ} = $circ;
951 $evt = _checkin_handle_circ($ctx);
954 } elsif( $transit ) {
956 # is this item currently in transit?
958 $ctx->{transit} = $transit;
959 $evt = $transcode->transit_receive( $copy, $requestor, $session );
961 if( !$U->event_equals($evt, 'SUCCESS') ) {
963 # either an error occurred or a ROUTE_ITEM was generated and the
964 # item must be forwarded on to its destination.
965 $evt->{payload}->{copy} = $U->unflesh_copy($copy);
972 # copy was received as a hold transit. Copy is at target lib
973 # and hold transit is complete. We're done here...
974 $U->commit_db_session($session);
975 return _checkin_flesh_event($ctx, $evt);
981 # If it's a renewal, we're not concerned with holds and transits.
982 # We just want the item to be checked in.
984 $U->commit_db_session($session);
985 return OpenILS::Event->new('SUCCESS');
988 # Now, let's see if this copy is needed for a hold
989 my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor );
993 $ctx->{hold} = $hold;
996 # Capture the hold with this copy
997 return $evt if ($evt = _checkin_capture_hold($ctx));
999 if( $hold->pickup_lib == $requestor->home_ou ) {
1001 # This hold was captured in the correct location
1002 $evt = OpenILS::Event->new('SUCCESS');
1006 # Hold needs to be picked up elsewhere. Build a hold
1007 # transit and route the item.
1008 return $evt if ($evt =_checkin_build_hold_transit($ctx));
1009 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1012 } else { # not needed for a hold
1014 if( $copy->circ_lib == $requestor->home_ou ) {
1016 # Copy is in the right place.
1017 $evt = OpenILS::Event->new('SUCCESS');
1018 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED') if $ctx->{precat};
1022 # Copy wants to go home. Transit it there.
1023 return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1024 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1029 $logger->info("Copy checkin finished with event: ".$evt->{textcode});
1031 return _checkin_flesh_event(
1032 $ctx, OpenILS::Event->new('NO_CHANGE')) if $nochange;
1034 $U->commit_db_session($session);
1035 return _checkin_flesh_event($ctx, $evt);
1039 sub _checkin_check_copy_status {
1041 my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1042 my $evt = OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1043 return $evt if ($stat == $U->copy_status_from_name('lost')->id);
1044 return $evt if ($stat == $U->copy_status_from_name('missing')->id);
1048 # Just gets the copy back home. Returns undef on success, event on error
1049 sub _checkin_build_generic_copy_transit {
1052 my $requestor = $ctx->{requestor};
1053 my $copy = $ctx->{copy};
1054 my $transit = Fieldmapper::action::transit_copy->new;
1055 my $session = $ctx->{session};
1057 $logger->activity("User ". $requestor->id ." creating a ".
1058 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1060 $transit->source($requestor->home_ou);
1061 $transit->dest($copy->circ_lib);
1062 $transit->target_copy($copy->id);
1063 $transit->source_send_time('now');
1064 $transit->copy_status($copy->status);
1066 $logger->debug("Creating new copy_transit in DB");
1068 my $s = $session->request(
1069 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1070 return $U->DB_UPDATE_FAILED($transit) unless $s;
1072 $logger->info("Checkin copy successfully created new transit: $s");
1074 $copy->status($U->copy_status_from_name('in transit')->id );
1076 return $U->update_copy( copy => $copy,
1077 editor => $requestor->id, session => $session );
1082 sub _checkin_build_hold_transit {
1085 my $copy = $ctx->{copy};
1086 my $hold = $ctx->{hold};
1087 my $trans = Fieldmapper::action::hold_transit_copy->new;
1089 $trans->hold($hold->id);
1090 $trans->source($ctx->{requestor}->home_ou);
1091 $trans->dest($hold->pickup_lib);
1092 $trans->source_send_time("now");
1093 $trans->target_copy($copy->id);
1094 $trans->copy_status($copy->status);
1096 my $id = $ctx->{session}->request(
1097 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1098 return $U->DB_UPDATE_FAILED($trans) unless $id;
1100 $logger->info("Checkin copy successfully created hold transit: $id");
1102 $copy->status($U->copy_status_from_name('in transit')->id );
1103 return $U->update_copy( copy => $copy,
1104 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1107 # Returns event on error, undef on success
1108 sub _checkin_capture_hold {
1110 my $copy = $ctx->{copy};
1111 my $hold = $ctx->{hold};
1113 $logger->debug("Checkin copy capturing hold ".$hold->id);
1115 $hold->current_copy($copy->id);
1116 $hold->capture_time('now');
1118 my $stat = $ctx->{session}->request(
1119 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1120 return $U->DB_UPDATE_FAILED($hold) unless $stat;
1122 $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1124 return $U->update_copy( copy => $copy,
1125 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1128 sub _checkin_flesh_event {
1133 $payload->{copy} = $U->unflesh_copy($ctx->{copy});
1134 $payload->{record} = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1135 $payload->{circ} = $ctx->{circ} if $ctx->{circ};
1136 $payload->{transit} = $ctx->{transit} if $ctx->{transit};
1138 $evt->{payload} = $payload;
1143 # ------------------------------------------------------------------------------
1146 __PACKAGE__->register_method(
1147 method => "checkin",
1148 api_name => "open-ils.circ.checkin___",
1149 notes => <<" NOTES");
1150 PARAMS( authtoken, barcode => bc )
1151 Checks in based on barcode
1152 Returns an event object whose payload contains the record, circ, and copy
1153 If the item needs to be routed, the event is a ROUTE_ITEM event
1154 with an additional 'route_to' variable set on the event
1158 my( $self, $client, $authtoken, $params ) = @_;
1161 my( $ctx, $requestor, $evt, $circ, $copy, $payload, $transit );
1163 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
1164 ( $requestor, $evt ) = $U->checksesperm(
1165 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
1166 return $evt if $evt;
1168 if( !( $ctx = $params->{_ctx}) ) {
1169 ( $ctx, $evt ) = create_circ_ctx( %$params,
1170 requestor => $requestor,
1171 session => $U->start_db_session(),
1173 #fetch_patron_circ_summary => 1,
1174 fetch_copy_statuses => 1,
1175 fetch_copy_locations => 1,
1178 return $evt if $evt;
1180 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
1181 $ctx->{authtoken} = $authtoken;
1183 $copy = $ctx->{copy};
1184 return OpenILS::Event->new('COPY_NOT_FOUND') unless $copy;
1186 $payload = { copy => $copy };
1187 $payload->{record} =
1188 $U->record_to_mvr($ctx->{title})
1189 if($ctx->{title} and !$ctx->{precat});
1191 # if( $copy->status ==
1192 # $U->copy_status_from_name($cache{copy_statuses}, 'lost')->id) {
1194 # } else { $__islost = 0; }
1196 my $status = $U->copy_status_from_name('in transit');
1197 if( $copy->status == $status->id ) {
1199 # if this copy is in transit, send it to transit_receive.
1200 $evt = $transcode->transit_receive( $copy, $requestor, $ctx->{session} );
1201 if( !$U->event_equals($evt, 'SUCCESS')) {
1202 $evt->{payload}->{copy} = $U->unflesh_copy($copy);
1208 # set the status to available for now for ease of debugging
1209 $copy->status( $U->copy_status_from_name('available')->id );
1211 # set the status to reshelving XXX needs to fall back to
1212 # 'available' after a configurable amount of time
1213 #$copy->status( $U->copy_status_from_name('reshelving')->id );
1215 # grab the open circ attached to this copy
1216 ( $circ, $evt ) = $U->fetch_open_circulation($copy->id);
1218 $evt->{payload} = $payload;
1219 $evt->{payload}->{copy} = $U->unflesh_copy($copy);
1223 $ctx->{circ} = $circ;
1224 $payload->{circ} = $circ;
1226 # update the circ and copy in the db
1227 return $evt if($evt = _update_checkin_circ_and_copy($ctx));
1229 # ------------------------------------------------------------------------------
1230 # If we get to this point, then the checkin will succeed. We just need to
1231 # see if there is any other processing required on this copy
1232 # ------------------------------------------------------------------------------
1235 if( !($evt = _check_checkin_holds($ctx)) ) {
1236 # if no hold is found for the copy, see if it needs to be transited
1237 ($evt, $transit) = $self->check_copy_transit($ctx);
1238 return $evt if ($evt and !$transit);
1239 $payload->{transit} = $transit if $transit;
1243 $logger->debug("Checkin succeeded. Committing objects...");
1244 $U->commit_db_session($ctx->{session});
1246 # if the item is not cataloged and no superceding
1247 # events exist, return the proper event
1248 if ( $copy->call_number == -1 and !$evt ) {
1249 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED') }
1251 $evt = OpenILS::Event->new('SUCCESS') if (!$evt or $__isrenewal);
1252 $evt->{payload} = $payload;
1254 $logger->info("Checkin of copy ".$copy->id." returned event: ".$evt->{textcode});
1256 $evt->{payload}->{copy} = $U->unflesh_copy($copy);
1261 # returns (undef) if no transit is needed
1262 # returns (ROUTE_ITEM, $transit) on succsessful transit creation
1263 # return (other event) on failure
1264 sub check_copy_transit {
1265 my( $self, $ctx ) = @_;
1266 my $copy = $ctx->{copy};
1268 return (undef) if( $copy->circ_lib == $ctx->{requestor}->home_ou );
1270 my ($evt) = $self->method_lookup(
1271 'open-ils.circ.copy_transit.create')->run(
1273 { session => $ctx->{session}, copyid => $copy->id } );
1275 return ($evt, undef) unless $U->event_equals($evt,'SUCCESS');
1277 my $transit = $evt->{payload}->{transit};
1278 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib );
1279 return ($evt, $transit);
1282 sub _check_checkin_holds {
1285 my $session = $ctx->{session};
1286 my $requestor = $ctx->{requestor};
1287 my $copy = $ctx->{copy};
1289 $logger->debug("Searching for a local hold on a copy: " . $session->session_id);
1291 my ( $hold, $evt ) =
1292 $holdcode->find_local_hold( $session, $copy, $requestor );
1295 $evt = OpenILS::Event->new(
1296 'COPY_NEEDED_FOR_HOLD', org => $hold->pickup_lib);
1303 sub _checkin_handle_circ { return _update_checkin_circ_and_copy(@_); }
1305 sub _update_checkin_circ_and_copy {
1309 $logger->info("Handling circulation found in checkin...");
1311 my $circ = $ctx->{circ};
1312 my $copy = $ctx->{copy};
1313 my $requestor = $ctx->{requestor};
1314 my $session = $ctx->{session};
1316 my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1317 return $evt if $evt;
1319 $circ->stop_fines('CHECKIN');
1320 $circ->stop_fines('RENEW') if $__isrenewal;
1321 $circ->stop_fines('LOST') if($__islost);
1322 $circ->xact_finish('now') if($obt->balance_owed <= 0 and !$__islost);
1323 $circ->stop_fines_time('now');
1324 $circ->checkin_time('now');
1325 $circ->checkin_staff($requestor->id);
1327 # if the requestor set a backdate, void all the bills after
1329 if(my $backdate = $ctx->{backdate}) {
1331 $logger->activity("User ".$requestor->id.
1332 " backdating checkin copy [".$ctx->{barcode}."] to date: $backdate");
1334 $circ->xact_finish($backdate);
1336 my $bills = $session->request( # XXX Verify this call is correct
1337 "open-ils.storage.direct.money.billing.search_where.atomic",
1338 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1341 for my $bill (@$bills) {
1343 my $s = $session->request(
1344 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1345 return $U->DB_UPDATE_FAILED($bill) unless $s;
1350 $logger->info("Checkin copy setting status to 'reshelving' and committing...");
1351 $copy->status($U->copy_status_from_name('reshelving')->id);
1352 $evt = $U->update_copy( session => $session, copy => $copy, editor => $requestor->id );
1353 return $evt if $evt;
1355 $ctx->{session}->request(
1356 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1363 # ------------------------------------------------------------------------------
1365 __PACKAGE__->register_method(
1367 api_name => "open-ils.circ.renew",
1368 notes => <<" NOTES");
1369 PARAMS( authtoken, circ => circ_id );
1370 open-ils.circ.renew(login_session, circ_object);
1371 Renews the provided circulation. login_session is the requestor of the
1372 renewal and if the logged in user is not the same as circ->usr, then
1373 the logged in user must have RENEW_CIRC permissions.
1377 my( $self, $client, $authtoken, $params ) = @_;
1380 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1383 # if requesting a renewal for someone else, you must have
1385 ( $requestor, $patron, $evt ) = $U->checkses_requestor(
1386 $authtoken, $params->{patron}, 'RENEW_CIRC' );
1387 if($evt) { $__isrenewal = 0; return $evt; }
1390 # fetch and build the circulation environment
1391 ( $ctx, $evt ) = create_circ_ctx( %$params,
1393 requestor => $requestor,
1396 fetch_patron_circ_summary => 1,
1397 fetch_copy_statuses => 1,
1398 fetch_copy_locations => 1,
1400 if($evt) { $__isrenewal = 0; return $evt; }
1401 $params->{_ctx} = $ctx;
1403 # make sure they have some renewals left and make sure the circulation exists
1404 ($circ, $evt) = _check_renewal_remaining($ctx);
1405 if($evt) { $__isrenewal = 0; return $evt; }
1406 $ctx->{old_circ} = $circ;
1407 my $renewals = $circ->renewal_remaining - 1;
1409 # run the renew permit script
1410 $evt = _run_renew_scripts($ctx);
1411 if($evt) { $__isrenewal = 0; return $evt; }
1414 #$ctx->{patron} = $ctx->{patron}->id;
1415 $evt = $self->checkin($client, $authtoken, $ctx );
1416 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1418 if( !$U->event_equals($evt, 'SUCCESS') ) {
1419 $__isrenewal = 0; return $evt;
1422 # re-fetch the context since objects have changed in the checkin
1423 ( $ctx, $evt ) = create_circ_ctx( %$params,
1425 requestor => $requestor,
1428 fetch_patron_circ_summary => 1,
1429 fetch_copy_statuses => 1,
1430 fetch_copy_locations => 1,
1432 if($evt) { $__isrenewal = 0; return $evt; }
1433 $params->{_ctx} = $ctx;
1434 $ctx->{renewal_remaining} = $renewals;
1436 # run the circ permit scripts
1437 $evt = $self->permit_circ( $client, $authtoken, $params );
1438 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1441 if(!$U->event_equals($evt, 'SUCCESS')) {
1442 if($evt) { $__isrenewal = 0; return $evt; }
1445 $params->{permit_key} = $evt->{payload};
1448 # checkout the item again
1449 $evt = $self->checkout($client, $authtoken, $params );
1455 sub _check_renewal_remaining {
1458 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1459 return (undef, $evt) if $evt;
1460 $evt = OpenILS::Event->new(
1461 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1462 return ($circ, $evt);
1465 sub _run_renew_scripts {
1467 my $runner = $ctx->{runner};
1470 $runner->load($scripts{circ_permit_renew});
1471 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1472 my $evtname = $runner->retrieve('result.event');
1473 $logger->activity("circ_permit_renew for user ".$ctx->{patron}->id." returned event: $evtname");
1475 return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';