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' );
311 $runner->insert('environment.isRenewal', 1);
313 $runner->insert('environment.isRenewal', undef);
316 if($ctx->{ishold} ) {
317 $runner->insert('environment.isHold', 1);
319 $runner->insert('environment.isHold', undef)
322 if( $ctx->{noncat} ) {
323 $runner->insert('environment.isNonCat', 1);
324 $runner->insert('environment.nonCatType', $ctx->{noncat_type});
326 $runner->insert('environment.isNonCat', undef);
329 if(ref($ctx->{patron_circ_summary})) {
330 $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
331 $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
334 $ctx->{runner} = $runner;
339 sub _add_script_runner_methods {
342 my $runner = $ctx->{runner};
346 # allows a script to fetch a hold that is currently targeting the
348 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
350 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
351 $hold = undef unless $hold;
352 $runner->insert( $key, $hold, 1 );
358 # ------------------------------------------------------------------------------
360 __PACKAGE__->register_method(
361 method => "permit_circ",
362 api_name => "open-ils.circ.checkout.permit",
364 Determines if the given checkout can occur
365 @param authtoken The login session key
366 @param params A trailing hash of named params including
367 barcode : The copy barcode,
368 patron : The patron the checkout is occurring for,
369 renew : true or false - whether or not this is a renewal
370 @return The event that occurred during the permit check.
374 my( $self, $client, $authtoken, $params ) = @_;
377 my ( $requestor, $patron, $ctx, $evt, $circ );
379 # check permisson of the requestor
380 ( $requestor, $patron, $evt ) =
381 $U->checkses_requestor(
382 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
385 # fetch and build the circulation environment
386 if( !( $ctx = $params->{_ctx}) ) {
388 ( $ctx, $evt ) = create_circ_ctx( %$params,
390 requestor => $requestor,
392 fetch_patron_circ_summary => 1,
393 fetch_copy_statuses => 1,
394 fetch_copy_locations => 1,
399 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
400 ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
401 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
404 return _run_permit_scripts($ctx);
408 __PACKAGE__->register_method(
409 method => "check_title_hold",
410 api_name => "open-ils.circ.title_hold.is_possible",
412 Determines if a hold were to be placed by a given user,
413 whether or not said hold would have any potential copies
415 @param authtoken The login session key
416 @param params A hash of named params including:
417 patronid - the id of the hold recipient
418 titleid (brn) - the id of the title to be held
419 depth - the hold range depth (defaults to 0)
422 sub check_title_hold {
423 my( $self, $client, $authtoken, $params ) = @_;
424 my %params = %$params;
425 my $titleid = $params{titleid};
427 my ( $requestor, $patron, $evt ) = $U->checkses_requestor(
428 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
431 my $rangelib = $patron->home_ou;
432 my $depth = $params{depth} || 0;
434 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
436 my $org = $U->simplereq(
438 'open-ils.actor.org_unit.retrieve',
439 $authtoken, $requestor->home_ou );
445 while( $title = $U->storagereq(
446 'open-ils.storage.biblio.record_entry.ranged_tree',
447 $titleid, $rangelib, $depth, $limit, $offset ) ) {
449 last unless ref($title);
451 for my $cn (@{$title->call_numbers}) {
453 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
455 for my $copy (@{$cn->copies}) {
457 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
459 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
461 requestor => $requestor,
464 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
465 request_lib => $org } );
467 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
479 # Runs the patron and copy permit scripts
480 # if this is a non-cat circulation, the copy permit script
482 sub _run_permit_scripts {
484 my $runner = $ctx->{runner};
485 my $patronid = $ctx->{patron}->id;
486 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
489 $runner->load($scripts{circ_permit_patron});
490 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
491 my $evtname = $runner->retrieve('result.event');
492 $logger->activity("circ_permit_patron for user $patronid returned event: $evtname");
494 return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';
496 my $key = _cache_permit_key();
498 if( $ctx->{noncat} ) {
499 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
500 return OpenILS::Event->new('SUCCESS', payload => $key);
504 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
505 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
509 $logger->debug("Exiting circ permit early because request is for hold patron permit");
510 return OpenILS::Event->new('SUCCESS');
513 $runner->load($scripts{circ_permit_copy});
514 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
515 $evtname = $runner->retrieve('result.event');
516 $logger->activity("circ_permit_copy for user $patronid ".
517 "and copy $barcode returned event: $evtname");
519 return OpenILS::Event->new($evtname, payload => $key) if( $evtname eq 'SUCCESS' );
520 return OpenILS::Event->new($evtname);
523 # takes copyid, patronid, and requestor id
524 sub _cache_permit_key {
525 my $key = md5_hex( time() . rand() . "$$" );
526 $logger->debug("Setting circ permit key to $key");
527 $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
531 sub _check_permit_key {
533 $logger->debug("Fetching circ permit key $key");
534 my $k = "oils_permit_key_$key";
535 my $one = $cache_handle->get_cache($k);
536 $cache_handle->delete_cache($k);
537 return ($one) ? 1 : 0;
541 # ------------------------------------------------------------------------------
543 __PACKAGE__->register_method(
544 method => "checkout",
545 api_name => "open-ils.circ.checkout",
548 @param authtoken The login session key
549 @param params A named hash of params including:
551 barcode If no copy is provided, the copy is retrieved via barcode
552 copyid If no copy or barcode is provide, the copy id will be use
553 patron The patron's id
554 noncat True if this is a circulation for a non-cataloted item
555 noncat_type The non-cataloged type id
556 noncat_circ_lib The location for the noncat circ.
557 precat The item has yet to be cataloged
558 dummy_title The temporary title of the pre-cataloded item
559 dummy_author The temporary authr of the pre-cataloded item
560 Default is the home org of the staff member
561 @return The SUCCESS event on success, any other event depending on the error
565 my( $self, $client, $authtoken, $params ) = @_;
568 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
569 my $key = $params->{permit_key};
571 # if this is a renewal, then the requestor does not have to
572 # have checkout privelages
573 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
574 ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
577 $logger->debug("REQUESTOR event: " . ref($requestor));
579 if( $params->{patron} ) {
580 ( $patron, $evt ) = $U->fetch_user($params->{patron});
583 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
587 # set the circ lib to the home org of the requestor if not specified
588 my $circlib = (defined($params->{circ_lib})) ?
589 $params->{circ_lib} : $requestor->home_ou;
591 # if this is a non-cataloged item, check it out and return
592 return _checkout_noncat(
593 $key, $requestor, $patron, %$params ) if $params->{noncat};
595 # if this item has yet to be cataloged, make sure a dummy copy exists
596 ( $params->{copy}, $evt ) = _make_precat_copy(
597 $requestor, $circlib, $params ) if $params->{precat};
601 # fetch and build the circulation environment
602 if( !( $ctx = $params->{_ctx}) ) {
603 ( $ctx, $evt ) = create_circ_ctx( %$params,
605 requestor => $requestor,
606 session => $U->start_db_session(),
608 fetch_patron_circ_summary => 1,
609 fetch_copy_statuses => 1,
610 fetch_copy_locations => 1,
614 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
616 # if the call doesn't know it's not cataloged..
617 if(!$params->{precat}) {
618 if( $ctx->{copy}->call_number eq '-1' ) {
619 return OpenILS::Event->new('ITEM_NOT_CATALOGED');
623 # this happens in permit.. but we need to check here for 'offline' requests
624 ($circ) = $U->fetch_open_circulation($ctx->{copy}->id);
625 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
627 my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
629 if( $ctx->{permit_override} ) {
630 $evt = $U->check_perms(
631 $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
635 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
636 unless _check_permit_key($key);
639 $ctx->{circ_lib} = $circlib;
641 $evt = _run_checkout_scripts($ctx);
645 _build_checkout_circ_object($ctx);
647 $evt = _apply_modified_due_date($ctx);
650 $evt = _commit_checkout_circ_object($ctx);
653 $evt = _update_checkout_copy($ctx);
657 ($holds, $evt) = _handle_related_holds($ctx);
661 $logger->debug("Checkin committing objects with session thread trace: ".$ctx->{session}->session_id);
662 $U->commit_db_session($ctx->{session});
663 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
665 return OpenILS::Event->new('SUCCESS',
667 copy => $U->unflesh_copy($ctx->{copy}),
668 circ => $ctx->{circ},
670 holds_fulfilled => $holds,
675 sub _make_precat_copy {
676 my ( $requestor, $circlib, $params ) = @_;
678 my( $copy, undef ) = _find_copy_by_attr(%$params);
681 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
682 return ($copy, undef);
685 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
687 my $evt = OpenILS::Event->new(
688 'BAD_PARAMS', desc => "Dummy title or author not provided" )
689 unless ( $params->{dummy_title} and $params->{dummy_author} );
690 return (undef, $evt) if $evt;
692 $copy = Fieldmapper::asset::copy->new;
693 $copy->circ_lib($circlib);
694 $copy->creator($requestor->id);
695 $copy->editor($requestor->id);
696 $copy->barcode($params->{barcode});
697 $copy->call_number(-1); #special CN for precat materials
698 $copy->loan_duration(&PRECAT_LOAN_DURATION);
699 $copy->fine_level(&PRECAT_FINE_LEVEL);
701 $copy->dummy_title($params->{dummy_title});
702 $copy->dummy_author($params->{dummy_author});
704 my $id = $U->storagereq(
705 'open-ils.storage.direct.asset.copy.create', $copy );
706 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
708 $logger->debug("Pre-cataloged copy successfully created");
709 return $U->fetch_copy($id);
713 sub _run_checkout_scripts {
719 my $runner = $ctx->{runner};
721 $runner->insert('result.durationLevel');
722 $runner->insert('result.durationRule');
723 $runner->insert('result.recurringFinesRule');
724 $runner->insert('result.recurringFinesLevel');
725 $runner->insert('result.maxFine');
727 $runner->load($scripts{circ_duration});
728 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
729 my $duration = $runner->retrieve('result.durationRule');
730 $logger->debug("Circ duration script yielded a duration rule of: $duration");
732 $runner->load($scripts{circ_recurring_fines});
733 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
734 my $recurring = $runner->retrieve('result.recurringFinesRule');
735 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
737 $runner->load($scripts{circ_max_fines});
738 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
739 my $max_fine = $runner->retrieve('result.maxFine');
740 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
742 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
744 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
746 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
749 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
750 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
751 $ctx->{duration_rule} = $duration;
752 $ctx->{recurring_fines_rule} = $recurring;
753 $ctx->{max_fine_rule} = $max_fine;
758 sub _build_checkout_circ_object {
762 my $circ = new Fieldmapper::action::circulation;
763 my $duration = $ctx->{duration_rule};
764 my $max = $ctx->{max_fine_rule};
765 my $recurring = $ctx->{recurring_fines_rule};
766 my $copy = $ctx->{copy};
767 my $patron = $ctx->{patron};
768 my $dur_level = $ctx->{duration_level};
769 my $rec_level = $ctx->{recurring_fines_level};
771 $circ->duration( $duration->shrt ) if ($dur_level == 1);
772 $circ->duration( $duration->normal ) if ($dur_level == 2);
773 $circ->duration( $duration->extended ) if ($dur_level == 3);
775 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
776 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
777 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
779 $circ->duration_rule( $duration->name );
780 $circ->recuring_fine_rule( $recurring->name );
781 $circ->max_fine_rule( $max->name );
782 $circ->max_fine( $max->amount );
784 $circ->fine_interval($recurring->recurance_interval);
785 $circ->renewal_remaining( $duration->max_renewals );
786 $circ->target_copy( $copy->id );
787 $circ->usr( $patron->id );
788 $circ->circ_lib( $ctx->{circ_lib} );
791 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
792 $circ->opac_renewal(1);
793 $circ->renewal_remaining($ctx->{renewal_remaining});
794 $circ->circ_staff($ctx->{requestor}->id);
798 # if the user provided an overiding checkout time,
799 # (e.g. the checkout really happened several hours ago), then
800 # we apply that here. Does this need a perm??
801 if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
802 $logger->debug("circ setting checkout_time to $ds");
803 $circ->xact_start($ds);
806 # if a patron is renewing, 'requestor' will be the patron
807 $circ->circ_staff($ctx->{requestor}->id );
808 _set_circ_due_date($circ);
809 $ctx->{circ} = $circ;
812 sub _apply_modified_due_date {
814 my $circ = $ctx->{circ};
816 if( $ctx->{due_date} ) {
818 my $evt = $U->check_perms(
819 $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
822 my $ds = _create_date_stamp($ctx->{due_date});
823 $logger->debug("circ modifying due_date to $ds");
824 $circ->due_date($ds);
830 sub _create_date_stamp {
831 my $datestring = shift;
832 return undef unless $datestring;
833 $datestring = clense_ISO8601($datestring);
834 $logger->debug("circ created date stamp => $datestring");
838 sub _create_due_date {
839 my $duration = shift;
841 my ($sec,$min,$hour,$mday,$mon,$year) =
842 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
843 $year += 1900; $mon += 1;
844 my $due_date = sprintf(
845 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
846 $year, $mon, $mday, $hour, $min, $sec);
850 sub _set_circ_due_date {
853 my $dd = _create_due_date($circ->duration);
854 $logger->debug("Checkout setting due date on circ to: $dd");
855 $circ->due_date($dd);
858 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
859 sub _update_checkout_copy {
862 my $copy = $ctx->{copy};
864 my $s = $U->copy_status_from_name('checked out');
865 $copy->status( $s->id ) if $s;
867 my $evt = $U->update_copy( session => $ctx->{session},
868 copy => $copy, editor => $ctx->{requestor}->id );
869 return (undef,$evt) if $evt;
874 # commits the circ object to the db then fleshes the circ with rules objects
875 sub _commit_checkout_circ_object {
878 my $circ = $ctx->{circ};
882 my $r = $ctx->{session}->request(
883 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
885 return $U->DB_UPDATE_FAILED($circ) unless $r;
887 $logger->debug("Created a new circ object in checkout: $r");
890 $circ->duration_rule($ctx->{duration_rule});
891 $circ->max_fine_rule($ctx->{max_fine_rule});
892 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
898 # sees if there are any holds that this copy
899 sub _handle_related_holds {
902 my $copy = $ctx->{copy};
903 my $patron = $ctx->{patron};
904 my $holds = $holdcode->fetch_related_holds($copy->id);
908 # XXX We should only fulfill one hold here...
909 # XXX If a hold was transited to the user who is checking out
910 # the item, we need to make sure that hold is what's grabbed
911 if(ref($holds) && @$holds) {
913 # for now, just sort by id to get what should be the oldest hold
914 $holds = [ sort { $a->id <=> $b->id } @$holds ];
915 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
918 my $hold = $holds->[0];
920 $logger->debug("Related hold found in checkout: " . $hold->id );
922 $hold->current_copy($copy->id); # just make sure it's set
923 # if the hold was never officially captured, capture it.
924 $hold->capture_time('now') unless $hold->capture_time;
925 $hold->fulfillment_time('now');
926 my $r = $ctx->{session}->request(
927 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
928 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
929 push( @fulfilled, $hold->id );
933 return (\@fulfilled, undef);
936 sub _checkout_noncat {
937 my ( $key, $requestor, $patron, %params ) = @_;
938 my( $circ, $circlib, $evt );
941 $circlib = $params{noncat_circ_lib} || $requestor->home_ou;
943 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
944 unless _check_permit_key($key);
946 my $count = $params{noncat_count} || 1;
947 my $cotime = _create_date_stamp($params{checkout_time}) || "";
948 $logger->info("circ creating $count noncat circs with checkout time $cotime");
950 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
951 $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
955 return OpenILS::Event->new(
956 'SUCCESS', payload => { noncat_circ => $circ } );
960 __PACKAGE__->register_method(
961 method => "generic_receive",
962 api_name => "open-ils.circ.checkin",
965 Generic super-method for handling all copies
966 @param authtoken The login session key
967 @param params Hash of named parameters including:
968 barcode - The copy barcode
969 force - If true, copies in bad statuses will be checked in and give good statuses
974 sub generic_receive {
975 my( $self, $connection, $authtoken, $params ) = @_;
976 my( $ctx, $requestor, $evt );
978 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
979 ( $requestor, $evt ) = $U->checksesperm(
980 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
983 # load up the circ objects
984 if( !( $ctx = $params->{_ctx}) ) {
985 ( $ctx, $evt ) = create_circ_ctx( %$params,
986 requestor => $requestor,
987 session => $U->start_db_session(),
989 fetch_copy_statuses => 1,
990 fetch_copy_locations => 1,
995 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
996 $ctx->{authtoken} = $authtoken;
997 my $session = $ctx->{session};
999 my $copy = $ctx->{copy};
1000 $U->unflesh_copy($copy);
1001 return OpenILS::Event->new('COPY_NOT_FOUND') unless $copy;
1003 $logger->info("Checkin copy called by user ".
1004 $requestor->id." for copy ".$copy->id);
1006 return $self->checkin_do_receive($connection, $ctx);
1009 sub checkin_do_receive {
1011 my( $self, $connection, $ctx ) = @_;
1014 my $copy = $ctx->{copy};
1015 my $session = $ctx->{session};
1016 my $requestor = $ctx->{requestor};
1017 my $change = 0; # did we actually do anything?
1019 if(!$ctx->{force}) {
1020 return $evt if ($evt = _checkin_check_copy_status($copy));
1023 ($ctx->{circ}, $evt) = $U->fetch_open_circulation($copy->id);
1024 return $evt if ($evt and $__isrenewal); # renewals require a circulation
1026 ($ctx->{transit}) = $U->fetch_open_transit_by_copy($copy->id);
1028 if( $ctx->{circ} ) {
1030 # There is an open circ on this item, close it out.
1032 $evt = _checkin_handle_circ($ctx);
1033 return $evt if $evt;
1035 } elsif( $ctx->{transit} ) {
1037 # is this item currently in transit?
1039 $evt = $transcode->transit_receive( $copy, $requestor, $session );
1040 my $holdtrans = $evt->{holdtransit};
1041 ($ctx->{hold}) = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1043 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1045 # either an error occurred or a ROUTE_ITEM was generated and the
1046 # item must be forwarded on to its destination.
1047 return _checkin_flesh_event($ctx, $evt);
1053 # copy was received as a hold transit. Copy is at target lib
1054 # and hold transit is complete. We're done here...
1055 $U->commit_db_session($session);
1056 return _checkin_flesh_event($ctx, $evt);
1062 # ------------------------------------------------------------------------------
1063 # Circulations and transits are now closed where necessary. Now go on to see if
1064 # this copy can fulfill a hold or needs to be routed to a different location
1065 # ------------------------------------------------------------------------------
1068 # If it's a renewal, we're done
1070 $U->commit_db_session($session);
1071 return OpenILS::Event->new('SUCCESS');
1075 # Now, let's see if this copy is needed for a hold
1076 my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor );
1080 $ctx->{hold} = $hold;
1083 # Capture the hold with this copy
1084 return $evt if ($evt = _checkin_capture_hold($ctx));
1086 if( $hold->pickup_lib == $requestor->home_ou ) {
1088 # This hold was captured in the correct location
1089 $evt = OpenILS::Event->new('SUCCESS');
1093 # Hold needs to be picked up elsewhere. Build a hold
1094 # transit and route the item.
1095 return $evt if ($evt =_checkin_build_hold_transit($ctx));
1096 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1099 } else { # not needed for a hold
1101 if( $copy->circ_lib == $requestor->home_ou ) {
1103 # Copy is in the right place.
1104 $evt = OpenILS::Event->new('SUCCESS');
1106 # if the item happens to be a pre-cataloged item, send it
1107 # to cataloging and return the event
1108 my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1109 return $err if $err;
1115 # Copy wants to go home. Transit it there.
1116 return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1117 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1122 $logger->info("Copy checkin finished with event: ".$evt->{textcode});
1126 $evt = OpenILS::Event->new('NO_CHANGE');
1127 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id)
1128 if ($copy->status == $U->copy_status_from_name('on holds shelf')->id);
1132 $U->commit_db_session($session);
1135 return _checkin_flesh_event($ctx, $evt);
1138 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1139 sub _checkin_handle_precat {
1142 my $copy = $ctx->{copy};
1147 my $catstat = $U->copy_status_from_name('cataloging');
1149 if( $ctx->{precat} ) {
1151 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1153 if( $copy->status != $catstat->id ) {
1154 $copy->status($catstat->id);
1156 return (undef, 0, $errevt) if (
1157 $errevt = $U->update_copy(
1159 editor => $ctx->{requestor}->id,
1160 session => $ctx->{session} ));
1166 return ($evt, $change, undef);
1170 sub _checkin_check_copy_status {
1172 my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1173 my $evt = OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1174 return $evt if ($stat == $U->copy_status_from_name('lost')->id);
1175 return $evt if ($stat == $U->copy_status_from_name('missing')->id);
1179 # Just gets the copy back home. Returns undef on success, event on error
1180 sub _checkin_build_generic_copy_transit {
1183 my $requestor = $ctx->{requestor};
1184 my $copy = $ctx->{copy};
1185 my $transit = Fieldmapper::action::transit_copy->new;
1186 my $session = $ctx->{session};
1188 $logger->activity("User ". $requestor->id ." creating a ".
1189 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1191 $transit->source($requestor->home_ou);
1192 $transit->dest($copy->circ_lib);
1193 $transit->target_copy($copy->id);
1194 $transit->source_send_time('now');
1195 $transit->copy_status($copy->status);
1197 $logger->debug("Creating new copy_transit in DB");
1199 my $s = $session->request(
1200 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1201 return $U->DB_UPDATE_FAILED($transit) unless $s;
1203 $logger->info("Checkin copy successfully created new transit: $s");
1205 $copy->status($U->copy_status_from_name('in transit')->id );
1207 return $U->update_copy( copy => $copy,
1208 editor => $requestor->id, session => $session );
1213 # returns event on error, undef on success
1214 sub _checkin_build_hold_transit {
1217 my $copy = $ctx->{copy};
1218 my $hold = $ctx->{hold};
1219 my $trans = Fieldmapper::action::hold_transit_copy->new;
1221 $trans->hold($hold->id);
1222 $trans->source($ctx->{requestor}->home_ou);
1223 $trans->dest($hold->pickup_lib);
1224 $trans->source_send_time("now");
1225 $trans->target_copy($copy->id);
1226 $trans->copy_status($copy->status);
1228 my $id = $ctx->{session}->request(
1229 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1230 return $U->DB_UPDATE_FAILED($trans) unless $id;
1232 $logger->info("Checkin copy successfully created hold transit: $id");
1234 $copy->status($U->copy_status_from_name('in transit')->id );
1235 return $U->update_copy( copy => $copy,
1236 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1239 # Returns event on error, undef on success
1240 sub _checkin_capture_hold {
1242 my $copy = $ctx->{copy};
1243 my $hold = $ctx->{hold};
1245 $logger->debug("Checkin copy capturing hold ".$hold->id);
1247 $hold->current_copy($copy->id);
1248 $hold->capture_time('now');
1250 my $stat = $ctx->{session}->request(
1251 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1252 return $U->DB_UPDATE_FAILED($hold) unless $stat;
1254 $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1256 return $U->update_copy( copy => $copy,
1257 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1260 # fleshes an event with the relevant objects from the context
1261 sub _checkin_flesh_event {
1266 $payload->{copy} = $U->unflesh_copy($ctx->{copy});
1267 $payload->{record} = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1268 $payload->{circ} = $ctx->{circ} if $ctx->{circ};
1269 $payload->{transit} = $ctx->{transit} if $ctx->{transit};
1270 $payload->{hold} = $ctx->{hold} if $ctx->{hold};
1272 $evt->{payload} = $payload;
1277 # Closes out the circulation, puts the copy into reshelving.
1278 # Voids any bills attached to this circ after the backdate time
1279 # if a backdate is provided
1280 sub _checkin_handle_circ {
1284 my $circ = $ctx->{circ};
1285 my $copy = $ctx->{copy};
1286 my $requestor = $ctx->{requestor};
1287 my $session = $ctx->{session};
1289 $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1291 $ctx->{longoverdue} = 1 if ($circ->stop_fines =~ /longoverdue/io);
1292 $ctx->{claimsreturned} = 1 if ($circ->stop_fines =~ /claimsreturned/io);
1294 my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1295 return $evt if $evt;
1297 $circ->stop_fines('CHECKIN');
1298 $circ->stop_fines('RENEW') if $__isrenewal;
1299 $circ->stop_fines('LOST') if($__islost);
1300 $circ->xact_finish('now') if($obt->balance_owed <= 0 and !$__islost);
1301 $circ->stop_fines_time('now');
1302 $circ->checkin_time('now');
1303 $circ->checkin_staff($requestor->id);
1305 if(my $backdate = $ctx->{backdate}) {
1306 return $evt if ($evt =
1307 _checkin_handle_backdate($backdate, $circ, $requestor, $session));
1310 $logger->info("Checkin copy setting status to 'reshelving' and committing...");
1311 $copy->status($U->copy_status_from_name('reshelving')->id);
1312 $evt = $U->update_copy( session => $session,
1313 copy => $copy, editor => $requestor->id );
1314 return $evt if $evt;
1316 $ctx->{session}->request(
1317 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1322 # returns event on error, undef on success
1323 # This voids all bills attached to the given circulation that occurred
1324 # after the backdate
1325 sub _checkin_handle_backdate {
1326 my( $backdate, $circ, $requestor, $session ) = @_;
1328 $logger->activity("User ".$requestor->id.
1329 " backdating checkin copy [".$circ->target_copy."] to date: $backdate");
1331 $circ->xact_finish($backdate);
1333 my $bills = $session->request( # XXX Verify this call is correct
1334 "open-ils.storage.direct.money.billing.search_where.atomic",
1335 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1338 for my $bill (@$bills) {
1340 my $s = $session->request(
1341 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1342 return $U->DB_UPDATE_FAILED($bill) unless $s;
1351 # ------------------------------------------------------------------------------
1353 __PACKAGE__->register_method(
1355 api_name => "open-ils.circ.renew",
1356 notes => <<" NOTES");
1357 PARAMS( authtoken, circ => circ_id );
1358 open-ils.circ.renew(login_session, circ_object);
1359 Renews the provided circulation. login_session is the requestor of the
1360 renewal and if the logged in user is not the same as circ->usr, then
1361 the logged in user must have RENEW_CIRC permissions.
1365 my( $self, $client, $authtoken, $params ) = @_;
1368 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1371 # fetch the patron object one way or another
1372 if( $params->{patron} ) {
1373 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1374 if($evt) { $__isrenewal = 0; return $evt; }
1376 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1377 if($evt) { $__isrenewal = 0; return $evt; }
1380 # verify our login session
1381 ($requestor, $evt) = $U->checkses($authtoken);
1382 if($evt) { $__isrenewal = 0; return $evt; }
1384 # make sure we have permission to perform a renewal
1385 if( $requestor->id ne $patron->id ) {
1386 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'RENEW_CIRC');
1387 if($evt) { $__isrenewal = 0; return $evt; }
1391 # fetch and build the circulation environment
1392 ( $ctx, $evt ) = create_circ_ctx( %$params,
1394 requestor => $requestor,
1397 fetch_patron_circ_summary => 1,
1398 fetch_copy_statuses => 1,
1399 fetch_copy_locations => 1,
1401 if($evt) { $__isrenewal = 0; return $evt; }
1402 $params->{_ctx} = $ctx;
1404 # make sure they have some renewals left and make sure the circulation exists
1405 ($circ, $evt) = _check_renewal_remaining($ctx);
1406 if($evt) { $__isrenewal = 0; return $evt; }
1407 $ctx->{old_circ} = $circ;
1408 my $renewals = $circ->renewal_remaining - 1;
1410 # run the renew permit script
1411 $evt = _run_renew_scripts($ctx);
1412 if($evt) { $__isrenewal = 0; return $evt; }
1415 #$ctx->{patron} = $ctx->{patron}->id;
1416 $evt = $self->generic_receive($client, $authtoken, $ctx );
1417 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1419 if( !$U->event_equals($evt, 'SUCCESS') ) {
1420 $__isrenewal = 0; return $evt;
1423 # re-fetch the context since objects have changed in the checkin
1424 ( $ctx, $evt ) = create_circ_ctx( %$params,
1426 requestor => $requestor,
1429 fetch_patron_circ_summary => 1,
1430 fetch_copy_statuses => 1,
1431 fetch_copy_locations => 1,
1433 if($evt) { $__isrenewal = 0; return $evt; }
1434 $params->{_ctx} = $ctx;
1435 $ctx->{renewal_remaining} = $renewals;
1437 # run the circ permit scripts
1438 if( $ctx->{permit_override} ) {
1439 $evt = $U->check_perms(
1440 $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1441 if($evt) { $__isrenewal = 0; return $evt; }
1444 $evt = $self->permit_circ( $client, $authtoken, $params );
1445 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1449 if(!$U->event_equals($evt, 'SUCCESS')) {
1450 if($evt) { $__isrenewal = 0; return $evt; }
1453 $params->{permit_key} = $evt->{payload};
1457 # checkout the item again
1458 $evt = $self->checkout($client, $authtoken, $params );
1464 sub _check_renewal_remaining {
1467 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1468 return (undef, $evt) if $evt;
1469 $evt = OpenILS::Event->new(
1470 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1471 return ($circ, $evt);
1474 sub _run_renew_scripts {
1476 my $runner = $ctx->{runner};
1479 $runner->load($scripts{circ_permit_renew});
1480 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1481 my $evtname = $runner->retrieve('result.event');
1482 $logger->activity("circ_permit_renew for user ".$ctx->{patron}->id." returned event: $evtname");
1484 return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';