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};
600 # fetch and build the circulation environment
601 if( !( $ctx = $params->{_ctx}) ) {
602 ( $ctx, $evt ) = create_circ_ctx( %$params,
604 requestor => $requestor,
605 session => $U->start_db_session(),
607 fetch_patron_circ_summary => 1,
608 fetch_copy_statuses => 1,
609 fetch_copy_locations => 1,
613 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
615 my $cid = ($params->{precat}) ? -1 : $ctx->{copy}->id;
617 if( $ctx->{permit_override} ) {
618 $evt = $U->check_perms(
619 $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
623 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
624 unless _check_permit_key($key);
627 $ctx->{circ_lib} = $circlib;
629 $evt = _run_checkout_scripts($ctx);
632 _build_checkout_circ_object($ctx);
634 $evt = _apply_modified_due_date($ctx);
637 $evt = _commit_checkout_circ_object($ctx);
640 $evt = _update_checkout_copy($ctx);
644 ($holds, $evt) = _handle_related_holds($ctx);
648 $logger->debug("Checkin committing objects with session thread trace: ".$ctx->{session}->session_id);
649 $U->commit_db_session($ctx->{session});
650 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
652 return OpenILS::Event->new('SUCCESS',
654 copy => $U->unflesh_copy($ctx->{copy}),
655 circ => $ctx->{circ},
657 holds_fulfilled => $holds,
662 sub _make_precat_copy {
663 my ( $requestor, $circlib, $params ) = @_;
665 my( $copy, undef ) = _find_copy_by_attr(%$params);
668 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
669 return ($copy, undef);
672 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
674 my $evt = OpenILS::Event->new(
675 'BAD_PARAMS', desc => "Dummy title or author not provided" )
676 unless ( $params->{dummy_title} and $params->{dummy_author} );
677 return (undef, $evt) if $evt;
679 $copy = Fieldmapper::asset::copy->new;
680 $copy->circ_lib($circlib);
681 $copy->creator($requestor->id);
682 $copy->editor($requestor->id);
683 $copy->barcode($params->{barcode});
684 $copy->call_number(-1); #special CN for precat materials
685 $copy->loan_duration(&PRECAT_LOAN_DURATION); # these two should come from constants
686 $copy->fine_level(&PRECAT_FINE_LEVEL);
688 $copy->dummy_title($params->{dummy_title});
689 $copy->dummy_author($params->{dummy_author});
691 my $id = $U->storagereq(
692 'open-ils.storage.direct.asset.copy.create', $copy );
693 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
695 $logger->debug("Pre-cataloged copy successfully created");
696 return $U->fetch_copy($id);
700 sub _run_checkout_scripts {
706 my $runner = $ctx->{runner};
708 $runner->insert('result.durationLevel');
709 $runner->insert('result.durationRule');
710 $runner->insert('result.recurringFinesRule');
711 $runner->insert('result.recurringFinesLevel');
712 $runner->insert('result.maxFine');
714 $runner->load($scripts{circ_duration});
715 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
716 my $duration = $runner->retrieve('result.durationRule');
717 $logger->debug("Circ duration script yielded a duration rule of: $duration");
719 $runner->load($scripts{circ_recurring_fines});
720 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
721 my $recurring = $runner->retrieve('result.recurringFinesRule');
722 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
724 $runner->load($scripts{circ_max_fines});
725 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
726 my $max_fine = $runner->retrieve('result.maxFine');
727 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
729 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
731 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
733 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
736 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
737 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
738 $ctx->{duration_rule} = $duration;
739 $ctx->{recurring_fines_rule} = $recurring;
740 $ctx->{max_fine_rule} = $max_fine;
745 sub _build_checkout_circ_object {
749 my $circ = new Fieldmapper::action::circulation;
750 my $duration = $ctx->{duration_rule};
751 my $max = $ctx->{max_fine_rule};
752 my $recurring = $ctx->{recurring_fines_rule};
753 my $copy = $ctx->{copy};
754 my $patron = $ctx->{patron};
755 my $dur_level = $ctx->{duration_level};
756 my $rec_level = $ctx->{recurring_fines_level};
758 $circ->duration( $duration->shrt ) if ($dur_level == 1);
759 $circ->duration( $duration->normal ) if ($dur_level == 2);
760 $circ->duration( $duration->extended ) if ($dur_level == 3);
762 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
763 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
764 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
766 $circ->duration_rule( $duration->name );
767 $circ->recuring_fine_rule( $recurring->name );
768 $circ->max_fine_rule( $max->name );
769 $circ->max_fine( $max->amount );
771 $circ->fine_interval($recurring->recurance_interval);
772 $circ->renewal_remaining( $duration->max_renewals );
773 $circ->target_copy( $copy->id );
774 $circ->usr( $patron->id );
775 $circ->circ_lib( $ctx->{circ_lib} );
778 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
779 $circ->opac_renewal(1);
780 $circ->renewal_remaining($ctx->{renewal_remaining});
781 $circ->circ_staff($ctx->{requestor}->id);
785 # if the user provided an overiding checkout time,
786 # (e.g. the checkout really happened several hours ago), then
787 # we apply that here. Does this need a perm??
788 if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
789 $logger->debug("circ setting checkout_time to $ds");
790 $circ->xact_start($ds);
793 # if a patron is renewing, 'requestor' will be the patron
794 $circ->circ_staff($ctx->{requestor}->id );
795 _set_circ_due_date($circ);
796 $ctx->{circ} = $circ;
799 sub _apply_modified_due_date {
801 my $circ = $ctx->{circ};
803 if( $ctx->{due_date} ) {
805 my $evt = $U->check_perms(
806 $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
809 my $ds = _create_date_stamp($ctx->{due_date});
810 $logger->debug("circ modifying due_date to $ds");
811 $circ->due_date($ds);
817 sub _create_date_stamp {
818 my $datestring = shift;
819 return undef unless $datestring;
820 $datestring = clense_ISO8601($datestring);
821 $logger->debug("circ created date stamp => $datestring");
825 sub _create_due_date {
826 my $duration = shift;
828 my ($sec,$min,$hour,$mday,$mon,$year) =
829 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
830 $year += 1900; $mon += 1;
831 my $due_date = sprintf(
832 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
833 $year, $mon, $mday, $hour, $min, $sec);
837 sub _set_circ_due_date {
840 my $dd = _create_due_date($circ->duration);
841 $logger->debug("Checkout setting due date on circ to: $dd");
842 $circ->due_date($dd);
845 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
846 sub _update_checkout_copy {
849 my $copy = $ctx->{copy};
851 my $s = $U->copy_status_from_name('checked out');
852 $copy->status( $s->id ) if $s;
854 my $evt = $U->update_copy( session => $ctx->{session},
855 copy => $copy, editor => $ctx->{requestor}->id );
856 return (undef,$evt) if $evt;
861 # commits the circ object to the db then fleshes the circ with rules objects
862 sub _commit_checkout_circ_object {
865 my $circ = $ctx->{circ};
869 my $r = $ctx->{session}->request(
870 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
872 return $U->DB_UPDATE_FAILED($circ) unless $r;
874 $logger->debug("Created a new circ object in checkout: $r");
877 $circ->duration_rule($ctx->{duration_rule});
878 $circ->max_fine_rule($ctx->{max_fine_rule});
879 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
885 # sees if there are any holds that this copy
886 sub _handle_related_holds {
889 my $copy = $ctx->{copy};
890 my $patron = $ctx->{patron};
891 my $holds = $holdcode->fetch_related_holds($copy->id);
895 # XXX We should only fulfill one hold here...
896 # XXX If a hold was transited to the user who is checking out
897 # the item, we need to make sure that hold is what's grabbed
898 if(ref($holds) && @$holds) {
900 # for now, just sort by id to get what should be the oldest hold
901 $holds = [ sort { $a->id <=> $b->id } @$holds ];
902 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
905 my $hold = $holds->[0];
907 $logger->debug("Related hold found in checkout: " . $hold->id );
909 $hold->current_copy($copy->id); # just make sure it's set
910 # if the hold was never officially captured, capture it.
911 $hold->capture_time('now') unless $hold->capture_time;
912 $hold->fulfillment_time('now');
913 my $r = $ctx->{session}->request(
914 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
915 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
916 push( @fulfilled, $hold->id );
920 return (\@fulfilled, undef);
923 sub _checkout_noncat {
924 my ( $key, $requestor, $patron, %params ) = @_;
925 my( $circ, $circlib, $evt );
928 $circlib = $params{noncat_circ_lib} || $requestor->home_ou;
930 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
931 unless _check_permit_key($key);
933 my $count = $params{noncat_count} || 1;
934 my $cotime = _create_date_stamp($params{checkout_time}) || "";
935 $logger->info("circ creating $count noncat circs with checkout time $cotime");
937 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
938 $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
942 return OpenILS::Event->new(
943 'SUCCESS', payload => { noncat_circ => $circ } );
947 __PACKAGE__->register_method(
948 method => "generic_receive",
949 api_name => "open-ils.circ.checkin",
952 Generic super-method for handling all copies
953 @param authtoken The login session key
954 @param params Hash of named parameters including:
955 barcode - The copy barcode
956 force - If true, copies in bad statuses will be checked in and give good statuses
961 sub generic_receive {
962 my( $self, $connection, $authtoken, $params ) = @_;
963 my( $ctx, $requestor, $evt );
965 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
966 ( $requestor, $evt ) = $U->checksesperm(
967 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
970 # load up the circ objects
971 if( !( $ctx = $params->{_ctx}) ) {
972 ( $ctx, $evt ) = create_circ_ctx( %$params,
973 requestor => $requestor,
974 session => $U->start_db_session(),
976 fetch_copy_statuses => 1,
977 fetch_copy_locations => 1,
982 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
983 $ctx->{authtoken} = $authtoken;
984 my $session = $ctx->{session};
986 my $copy = $ctx->{copy};
987 $U->unflesh_copy($copy);
988 return OpenILS::Event->new('COPY_NOT_FOUND') unless $copy;
990 $logger->info("Checkin copy called by user ".
991 $requestor->id." for copy ".$copy->id);
993 return $self->checkin_do_receive($connection, $ctx);
996 sub checkin_do_receive {
998 my( $self, $connection, $ctx ) = @_;
1001 my $copy = $ctx->{copy};
1002 my $session = $ctx->{session};
1003 my $requestor = $ctx->{requestor};
1004 my $change = 0; # did we actually do anything?
1006 if(!$ctx->{force}) {
1007 return $evt if ($evt = _checkin_check_copy_status($copy));
1010 ($ctx->{circ}, $evt) = $U->fetch_open_circulation($copy->id);
1011 return $evt if ($evt and $__isrenewal); # renewals require a circulation
1013 ($ctx->{transit}) = $U->fetch_open_transit_by_copy($copy->id);
1015 if( $ctx->{circ} ) {
1017 # There is an open circ on this item, close it out.
1019 $evt = _checkin_handle_circ($ctx);
1020 return $evt if $evt;
1022 } elsif( $ctx->{transit} ) {
1024 # is this item currently in transit?
1026 $evt = $transcode->transit_receive( $copy, $requestor, $session );
1027 my $holdtrans = $evt->{holdtransit};
1028 ($ctx->{hold}) = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1030 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1032 # either an error occurred or a ROUTE_ITEM was generated and the
1033 # item must be forwarded on to its destination.
1034 return _checkin_flesh_event($ctx, $evt);
1040 # copy was received as a hold transit. Copy is at target lib
1041 # and hold transit is complete. We're done here...
1042 $U->commit_db_session($session);
1043 return _checkin_flesh_event($ctx, $evt);
1049 # ------------------------------------------------------------------------------
1050 # Circulations and transits are now closed where necessary. Now go on to see if
1051 # this copy can fulfill a hold or needs to be routed to a different location
1052 # ------------------------------------------------------------------------------
1055 # If it's a renewal, we're done
1057 $U->commit_db_session($session);
1058 return OpenILS::Event->new('SUCCESS');
1062 # Now, let's see if this copy is needed for a hold
1063 my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor );
1067 $ctx->{hold} = $hold;
1070 # Capture the hold with this copy
1071 return $evt if ($evt = _checkin_capture_hold($ctx));
1073 if( $hold->pickup_lib == $requestor->home_ou ) {
1075 # This hold was captured in the correct location
1076 $evt = OpenILS::Event->new('SUCCESS');
1080 # Hold needs to be picked up elsewhere. Build a hold
1081 # transit and route the item.
1082 return $evt if ($evt =_checkin_build_hold_transit($ctx));
1083 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1086 } else { # not needed for a hold
1088 if( $copy->circ_lib == $requestor->home_ou ) {
1090 # Copy is in the right place.
1091 $evt = OpenILS::Event->new('SUCCESS');
1093 # if the item happens to be a pre-cataloged item, send it
1094 # to cataloging and return the event
1095 my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1096 return $err if $err;
1102 # Copy wants to go home. Transit it there.
1103 return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1104 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1109 $logger->info("Copy checkin finished with event: ".$evt->{textcode});
1113 $evt = OpenILS::Event->new('NO_CHANGE');
1114 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id)
1115 if ($copy->status == $U->copy_status_from_name('on holds shelf')->id);
1119 $U->commit_db_session($session);
1122 return _checkin_flesh_event($ctx, $evt);
1125 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1126 sub _checkin_handle_precat {
1129 my $copy = $ctx->{copy};
1134 my $catstat = $U->copy_status_from_name('cataloging');
1136 if( $ctx->{precat} ) {
1138 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1140 if( $copy->status != $catstat->id ) {
1141 $copy->status($catstat->id);
1143 return (undef, 0, $errevt) if (
1144 $errevt = $U->update_copy(
1146 editor => $ctx->{requestor}->id,
1147 session => $ctx->{session} ));
1153 return ($evt, $change, undef);
1157 sub _checkin_check_copy_status {
1159 my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1160 my $evt = OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1161 return $evt if ($stat == $U->copy_status_from_name('lost')->id);
1162 return $evt if ($stat == $U->copy_status_from_name('missing')->id);
1166 # Just gets the copy back home. Returns undef on success, event on error
1167 sub _checkin_build_generic_copy_transit {
1170 my $requestor = $ctx->{requestor};
1171 my $copy = $ctx->{copy};
1172 my $transit = Fieldmapper::action::transit_copy->new;
1173 my $session = $ctx->{session};
1175 $logger->activity("User ". $requestor->id ." creating a ".
1176 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1178 $transit->source($requestor->home_ou);
1179 $transit->dest($copy->circ_lib);
1180 $transit->target_copy($copy->id);
1181 $transit->source_send_time('now');
1182 $transit->copy_status($copy->status);
1184 $logger->debug("Creating new copy_transit in DB");
1186 my $s = $session->request(
1187 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1188 return $U->DB_UPDATE_FAILED($transit) unless $s;
1190 $logger->info("Checkin copy successfully created new transit: $s");
1192 $copy->status($U->copy_status_from_name('in transit')->id );
1194 return $U->update_copy( copy => $copy,
1195 editor => $requestor->id, session => $session );
1200 # returns event on error, undef on success
1201 sub _checkin_build_hold_transit {
1204 my $copy = $ctx->{copy};
1205 my $hold = $ctx->{hold};
1206 my $trans = Fieldmapper::action::hold_transit_copy->new;
1208 $trans->hold($hold->id);
1209 $trans->source($ctx->{requestor}->home_ou);
1210 $trans->dest($hold->pickup_lib);
1211 $trans->source_send_time("now");
1212 $trans->target_copy($copy->id);
1213 $trans->copy_status($copy->status);
1215 my $id = $ctx->{session}->request(
1216 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1217 return $U->DB_UPDATE_FAILED($trans) unless $id;
1219 $logger->info("Checkin copy successfully created hold transit: $id");
1221 $copy->status($U->copy_status_from_name('in transit')->id );
1222 return $U->update_copy( copy => $copy,
1223 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1226 # Returns event on error, undef on success
1227 sub _checkin_capture_hold {
1229 my $copy = $ctx->{copy};
1230 my $hold = $ctx->{hold};
1232 $logger->debug("Checkin copy capturing hold ".$hold->id);
1234 $hold->current_copy($copy->id);
1235 $hold->capture_time('now');
1237 my $stat = $ctx->{session}->request(
1238 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1239 return $U->DB_UPDATE_FAILED($hold) unless $stat;
1241 $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1243 return $U->update_copy( copy => $copy,
1244 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1247 # fleshes an event with the relevant objects from the context
1248 sub _checkin_flesh_event {
1253 $payload->{copy} = $U->unflesh_copy($ctx->{copy});
1254 $payload->{record} = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1255 $payload->{circ} = $ctx->{circ} if $ctx->{circ};
1256 $payload->{transit} = $ctx->{transit} if $ctx->{transit};
1257 $payload->{hold} = $ctx->{hold} if $ctx->{hold};
1259 $evt->{payload} = $payload;
1264 # Closes out the circulation, puts the copy into reshelving.
1265 # Voids any bills attached to this circ after the backdate time
1266 # if a backdate is provided
1267 sub _checkin_handle_circ {
1271 my $circ = $ctx->{circ};
1272 my $copy = $ctx->{copy};
1273 my $requestor = $ctx->{requestor};
1274 my $session = $ctx->{session};
1276 $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1278 $ctx->{longoverdue} = 1 if ($circ->stop_fines =~ /longoverdue/io);
1279 $ctx->{claimsreturned} = 1 if ($circ->stop_fines =~ /claimsreturned/io);
1281 my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1282 return $evt if $evt;
1284 $circ->stop_fines('CHECKIN');
1285 $circ->stop_fines('RENEW') if $__isrenewal;
1286 $circ->stop_fines('LOST') if($__islost);
1287 $circ->xact_finish('now') if($obt->balance_owed <= 0 and !$__islost);
1288 $circ->stop_fines_time('now');
1289 $circ->checkin_time('now');
1290 $circ->checkin_staff($requestor->id);
1292 if(my $backdate = $ctx->{backdate}) {
1293 return $evt if ($evt =
1294 _checkin_handle_backdate($backdate, $circ, $requestor, $session));
1297 $logger->info("Checkin copy setting status to 'reshelving' and committing...");
1298 $copy->status($U->copy_status_from_name('reshelving')->id);
1299 $evt = $U->update_copy( session => $session,
1300 copy => $copy, editor => $requestor->id );
1301 return $evt if $evt;
1303 $ctx->{session}->request(
1304 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1309 # returns event on error, undef on success
1310 # This voids all bills attached to the given circulation that occurred
1311 # after the backdate
1312 sub _checkin_handle_backdate {
1313 my( $backdate, $circ, $requestor, $session ) = @_;
1315 $logger->activity("User ".$requestor->id.
1316 " backdating checkin copy [".$circ->target_copy."] to date: $backdate");
1318 $circ->xact_finish($backdate);
1320 my $bills = $session->request( # XXX Verify this call is correct
1321 "open-ils.storage.direct.money.billing.search_where.atomic",
1322 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1325 for my $bill (@$bills) {
1327 my $s = $session->request(
1328 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1329 return $U->DB_UPDATE_FAILED($bill) unless $s;
1338 # ------------------------------------------------------------------------------
1340 __PACKAGE__->register_method(
1342 api_name => "open-ils.circ.renew",
1343 notes => <<" NOTES");
1344 PARAMS( authtoken, circ => circ_id );
1345 open-ils.circ.renew(login_session, circ_object);
1346 Renews the provided circulation. login_session is the requestor of the
1347 renewal and if the logged in user is not the same as circ->usr, then
1348 the logged in user must have RENEW_CIRC permissions.
1352 my( $self, $client, $authtoken, $params ) = @_;
1355 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1358 # fetch the patron object one way or another
1359 if( $params->{patron} ) {
1360 ( $patron, $evt ) = $U->fetch_user($params->{patron});
1361 if($evt) { $__isrenewal = 0; return $evt; }
1363 ( $patron, $evt ) = $U->fetch_user_by_barcode($params->{patron_barcode});
1364 if($evt) { $__isrenewal = 0; return $evt; }
1367 # verify our login session
1368 ($requestor, $evt) = $U->checkses($authtoken);
1369 if($evt) { $__isrenewal = 0; return $evt; }
1371 # make sure we have permission to perform a renewal
1372 if( $requestor->id ne $patron->id ) {
1373 $evt = $U->check_perms($requestor->id, $patron->home_ou, 'RENEW_CIRC');
1374 if($evt) { $__isrenewal = 0; return $evt; }
1378 # fetch and build the circulation environment
1379 ( $ctx, $evt ) = create_circ_ctx( %$params,
1381 requestor => $requestor,
1384 fetch_patron_circ_summary => 1,
1385 fetch_copy_statuses => 1,
1386 fetch_copy_locations => 1,
1388 if($evt) { $__isrenewal = 0; return $evt; }
1389 $params->{_ctx} = $ctx;
1391 # make sure they have some renewals left and make sure the circulation exists
1392 ($circ, $evt) = _check_renewal_remaining($ctx);
1393 if($evt) { $__isrenewal = 0; return $evt; }
1394 $ctx->{old_circ} = $circ;
1395 my $renewals = $circ->renewal_remaining - 1;
1397 # run the renew permit script
1398 $evt = _run_renew_scripts($ctx);
1399 if($evt) { $__isrenewal = 0; return $evt; }
1402 #$ctx->{patron} = $ctx->{patron}->id;
1403 $evt = $self->generic_receive($client, $authtoken, $ctx );
1404 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1406 if( !$U->event_equals($evt, 'SUCCESS') ) {
1407 $__isrenewal = 0; return $evt;
1410 # re-fetch the context since objects have changed in the checkin
1411 ( $ctx, $evt ) = create_circ_ctx( %$params,
1413 requestor => $requestor,
1416 fetch_patron_circ_summary => 1,
1417 fetch_copy_statuses => 1,
1418 fetch_copy_locations => 1,
1420 if($evt) { $__isrenewal = 0; return $evt; }
1421 $params->{_ctx} = $ctx;
1422 $ctx->{renewal_remaining} = $renewals;
1424 # run the circ permit scripts
1425 if( $ctx->{permit_override} ) {
1426 $evt = $U->check_perms(
1427 $requestor->id, $ctx->{copy}->circ_lib->id, 'CIRC_PERMIT_OVERRIDE');
1428 if($evt) { $__isrenewal = 0; return $evt; }
1431 $evt = $self->permit_circ( $client, $authtoken, $params );
1432 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1436 if(!$U->event_equals($evt, 'SUCCESS')) {
1437 if($evt) { $__isrenewal = 0; return $evt; }
1440 $params->{permit_key} = $evt->{payload};
1444 # checkout the item again
1445 $evt = $self->checkout($client, $authtoken, $params );
1451 sub _check_renewal_remaining {
1454 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1455 return (undef, $evt) if $evt;
1456 $evt = OpenILS::Event->new(
1457 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1458 return ($circ, $evt);
1461 sub _run_renew_scripts {
1463 my $runner = $ctx->{runner};
1466 $runner->load($scripts{circ_permit_renew});
1467 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1468 my $evtname = $runner->retrieve('result.event');
1469 $logger->activity("circ_permit_renew for user ".$ctx->{patron}->id." returned event: $evtname");
1471 return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';