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; }
34 # for security, this is a process-defined and not
35 # a client-defined variable
39 # ------------------------------------------------------------------------------
40 # Load the circ script from the config
41 # ------------------------------------------------------------------------------
45 $cache_handle = OpenSRF::Utils::Cache->new('global');
46 my $conf = OpenSRF::Utils::SettingsClient->new;
47 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
48 my @pfx = ( @pfx2, "scripts" );
50 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
51 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
52 my $d = $conf->config_value( @pfx, 'circ_duration' );
53 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
54 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
55 my $pr = $conf->config_value( @pfx, 'circ_permit_renew' );
56 my $lb = $conf->config_value( @pfx2, 'script_path' );
58 $logger->error( "Missing circ script(s)" )
59 unless( $p and $c and $d and $f and $m and $pr );
61 $scripts{circ_permit_patron} = $p;
62 $scripts{circ_permit_copy} = $c;
63 $scripts{circ_duration} = $d;
64 $scripts{circ_recurring_fines}= $f;
65 $scripts{circ_max_fines} = $m;
66 $scripts{circ_permit_renew} = $pr;
68 $lb = [ $lb ] unless ref($lb);
71 $logger->debug("Loaded rules scripts for circ: " .
72 "circ permit patron: $p, circ permit copy: $c, ".
73 "circ duration :$d , circ recurring fines : $f, " .
74 "circ max fines : $m, circ renew permit : $pr");
78 # ------------------------------------------------------------------------------
79 # Loads the necessary circ objects and pushes them into the script environment
80 # Returns ( $data, $evt ). if $evt is defined, then an
81 # unexpedted event occurred and should be dealt with / returned to the caller
82 # ------------------------------------------------------------------------------
90 $evt = _ctx_add_patron_objects($ctx, %params);
91 return (undef,$evt) if $evt;
93 if(!$params{noncat}) {
94 if( $evt = _ctx_add_copy_objects($ctx, %params) ) {
95 $ctx->{precat} = 1 if($evt->{textcode} eq 'COPY_NOT_FOUND')
97 $ctx->{precat} = 1 if ( $ctx->{copy}->call_number == -1 ); # special case copy
101 _doctor_patron_object($ctx) if $ctx->{patron};
102 _doctor_copy_object($ctx) if $ctx->{copy};
104 if(!$ctx->{no_runner}) {
105 _build_circ_script_runner($ctx);
106 _add_script_runner_methods($ctx);
112 sub _ctx_add_patron_objects {
113 my( $ctx, %params) = @_;
116 if(!defined($cache{patron_standings})) {
117 $cache{patron_standings} = $U->fetch_patron_standings();
118 $cache{group_tree} = $U->fetch_permission_group_tree();
121 $ctx->{patron_standings} = $cache{patron_standings};
122 $ctx->{group_tree} = $cache{group_tree};
124 $ctx->{patron_circ_summary} =
125 $U->fetch_patron_circ_summary($ctx->{patron}->id)
126 if $params{fetch_patron_circsummary};
132 sub _find_copy_by_attr {
137 my $copy = $params{copy} || undef;
142 $U->fetch_copy($params{copyid}) if $params{copyid};
143 return (undef,$evt) if $evt;
147 $U->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
148 return (undef,$evt) if $evt;
151 return ( $copy, $evt );
154 sub _ctx_add_copy_objects {
155 my($ctx, %params) = @_;
160 $cache{copy_statuses} = $U->fetch_copy_statuses
161 if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
163 $cache{copy_locations} = $U->fetch_copy_locations
164 if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
166 $ctx->{copy_statuses} = $cache{copy_statuses};
167 $ctx->{copy_locations} = $cache{copy_locations};
169 ($copy, $evt) = _find_copy_by_attr(%params);
172 if( $copy and !$ctx->{title} ) {
173 $logger->debug("Copy status: " . $copy->status);
174 ( $ctx->{title}, $evt ) = $U->fetch_record_by_copy( $copy->id );
176 $ctx->{copy} = $copy;
183 # ------------------------------------------------------------------------------
184 # Fleshes parts of the patron object
185 # ------------------------------------------------------------------------------
186 sub _doctor_copy_object {
189 my $copy = $ctx->{copy} || return undef;
191 $logger->debug("Doctoring copy object...");
193 # set the copy status to a status name
194 $copy->status( _get_copy_status( $copy, $ctx->{copy_statuses} ) );
196 # set the copy location to the location object
197 $copy->location( _get_copy_location( $copy, $ctx->{copy_locations} ) );
199 $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
203 # ------------------------------------------------------------------------------
204 # Fleshes parts of the patron object
205 # ------------------------------------------------------------------------------
206 sub _doctor_patron_object {
209 my $patron = $ctx->{patron} || return undef;
211 # push the standing object into the patron
212 if(ref($ctx->{patron_standings})) {
213 for my $s (@{$ctx->{patron_standings}}) {
214 if( $s->id eq $ctx->{patron}->standing ) {
215 $patron->standing($s);
216 $logger->debug("Set patron standing to ". $s->value);
221 # set the patron ptofile to the profile name
222 $patron->profile( _get_patron_profile(
223 $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
227 $U->fetch_org_unit( $patron->home_ou ) ) if $patron;
231 # recurse and find the patron profile name from the tree
232 # another option would be to grab the groups for the patron
233 # and cycle through those until the "profile" group has been found
234 sub _get_patron_profile {
235 my( $patron, $group_tree ) = @_;
236 return $group_tree if ($group_tree->id eq $patron->profile);
237 return undef unless ($group_tree->children);
239 for my $child (@{$group_tree->children}) {
240 my $ret = _get_patron_profile( $patron, $child );
246 sub _get_copy_status {
247 my( $copy, $cstatus ) = @_;
250 for my $status (@$cstatus) {
251 $s = $status if( $status->id eq $copy->status )
253 $logger->debug("Retrieving copy status: " . $s->name) if $s;
257 sub _get_copy_location {
258 my( $copy, $locations ) = @_;
261 for my $loc (@$locations) {
262 $l = $loc if $loc->id eq $copy->location;
264 $logger->debug("Retrieving copy location: " . $l->name ) if $l;
269 # ------------------------------------------------------------------------------
270 # Constructs and shoves data into the script environment
271 # ------------------------------------------------------------------------------
272 sub _build_circ_script_runner {
276 $logger->debug("Loading script environment for circulation");
279 if( $runner = $contexts{$ctx->{type}} ) {
280 $runner->refresh_context;
282 $runner = OpenILS::Utils::ScriptRunner->new;
283 $contexts{type} = $runner;
287 $logger->debug("Loading circ script lib path $_");
288 $runner->add_path( $_ );
291 # Note: inserting the number 0 into the script turns into the
292 # string "0", and thus evaluates to true in JS land
293 # inserting undef will insert "", which evaluates to false
295 $runner->insert( 'environment.patron', $ctx->{patron}, 1);
296 $runner->insert( 'environment.title', $ctx->{title}, 1);
297 $runner->insert( 'environment.copy', $ctx->{copy}, 1);
300 $runner->insert( 'result', {} );
301 $runner->insert( 'result.event', 'SUCCESS' );
304 $runner->insert('environment.isRenewal', 1);
306 $runner->insert('environment.isRenewal', undef);
309 if($ctx->{ishold} ) {
310 $runner->insert('environment.isHold', 1);
312 $runner->insert('environment.isHold', undef)
315 if( $ctx->{noncat} ) {
316 $runner->insert('environment.isNonCat', 1);
317 $runner->insert('environment.nonCatType', $ctx->{noncat_type});
319 $runner->insert('environment.isNonCat', undef);
322 if(ref($ctx->{patron_circ_summary})) {
323 $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
324 $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
327 $ctx->{runner} = $runner;
332 sub _add_script_runner_methods {
335 my $runner = $ctx->{runner};
339 # allows a script to fetch a hold that is currently targeting the
341 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
343 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
344 $hold = undef unless $hold;
345 $runner->insert( $key, $hold, 1 );
351 # ------------------------------------------------------------------------------
353 __PACKAGE__->register_method(
354 method => "permit_circ",
355 api_name => "open-ils.circ.checkout.permit",
357 Determines if the given checkout can occur
358 @param authtoken The login session key
359 @param params A trailing hash of named params including
360 barcode : The copy barcode,
361 patron : The patron the checkout is occurring for,
362 renew : true or false - whether or not this is a renewal
363 @return The event that occurred during the permit check.
367 my( $self, $client, $authtoken, $params ) = @_;
370 my ( $requestor, $patron, $ctx, $evt, $circ );
372 # check permisson of the requestor
373 ( $requestor, $patron, $evt ) =
374 $U->checkses_requestor(
375 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
378 # fetch and build the circulation environment
379 if( !( $ctx = $params->{_ctx}) ) {
381 ( $ctx, $evt ) = create_circ_ctx( %$params,
383 requestor => $requestor,
385 fetch_patron_circ_summary => 1,
386 fetch_copy_statuses => 1,
387 fetch_copy_locations => 1,
392 if( !$ctx->{ishold} and !$__isrenewal and $ctx->{copy} ) {
393 ($circ, $evt) = $U->fetch_open_circulation($ctx->{copy}->id);
394 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS') if $circ;
397 return _run_permit_scripts($ctx);
401 __PACKAGE__->register_method(
402 method => "check_title_hold",
403 api_name => "open-ils.circ.title_hold.is_possible",
405 Determines if a hold were to be placed by a given user,
406 whether or not said hold would have any potential copies
408 @param authtoken The login session key
409 @param params A hash of named params including:
410 patronid - the id of the hold recipient
411 titleid (brn) - the id of the title to be held
412 depth - the hold range depth (defaults to 0)
415 sub check_title_hold {
416 my( $self, $client, $authtoken, $params ) = @_;
417 my %params = %$params;
418 my $titleid = $params{titleid};
420 my ( $requestor, $patron, $evt ) = $U->checkses_requestor(
421 $authtoken, $params{patronid}, 'VIEW_HOLD_PERMIT' );
424 my $rangelib = $patron->home_ou;
425 my $depth = $params{depth} || 0;
427 $logger->debug("Fetching ranged title tree for title $titleid, org $rangelib, depth $depth");
429 my $org = $U->simplereq(
431 'open-ils.actor.org_unit.retrieve',
432 $authtoken, $requestor->home_ou );
438 while( $title = $U->storagereq(
439 'open-ils.storage.biblio.record_entry.ranged_tree',
440 $titleid, $rangelib, $depth, $limit, $offset ) ) {
442 last unless ref($title);
444 for my $cn (@{$title->call_numbers}) {
446 $logger->debug("Checking callnumber ".$cn->id." for hold fulfillment possibility");
448 for my $copy (@{$cn->copies}) {
450 $logger->debug("Checking copy ".$copy->id." for hold fulfillment possibility");
452 return 1 if OpenILS::Utils::PermitHold::permit_copy_hold(
454 requestor => $requestor,
457 title_descriptor => $title->fixed_fields, # this is fleshed into the title object
458 request_lib => $org } );
460 $logger->debug("Copy ".$copy->id." for hold fulfillment possibility failed...");
472 # Runs the patron and copy permit scripts
473 # if this is a non-cat circulation, the copy permit script
475 sub _run_permit_scripts {
477 my $runner = $ctx->{runner};
478 my $patronid = $ctx->{patron}->id;
479 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
482 $runner->load($scripts{circ_permit_patron});
483 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
484 my $evtname = $runner->retrieve('result.event');
485 $logger->activity("circ_permit_patron for user $patronid returned event: $evtname");
487 return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';
489 my $key = _cache_permit_key();
491 if( $ctx->{noncat} ) {
492 $logger->debug("Exiting circ permit early because item is a non-cataloged item");
493 return OpenILS::Event->new('SUCCESS', payload => $key);
497 $logger->debug("Exiting circ permit early because copy is pre-cataloged");
498 return OpenILS::Event->new('ITEM_NOT_CATALOGED', payload => $key);
502 $logger->debug("Exiting circ permit early because request is for hold patron permit");
503 return OpenILS::Event->new('SUCCESS');
506 $runner->load($scripts{circ_permit_copy});
507 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
508 $evtname = $runner->retrieve('result.event');
509 $logger->activity("circ_permit_copy for user $patronid ".
510 "and copy $barcode returned event: $evtname");
512 return OpenILS::Event->new($evtname, payload => $key) if( $evtname eq 'SUCCESS' );
513 return OpenILS::Event->new($evtname);
516 # takes copyid, patronid, and requestor id
517 sub _cache_permit_key {
518 my $key = md5_hex( time() . rand() . "$$" );
519 $logger->debug("Setting circ permit key to $key");
520 $cache_handle->put_cache( "oils_permit_key_$key", 1, 300 );
524 sub _check_permit_key {
526 $logger->debug("Fetching circ permit key $key");
527 my $k = "oils_permit_key_$key";
528 my $one = $cache_handle->get_cache($k);
529 $cache_handle->delete_cache($k);
530 return ($one) ? 1 : 0;
534 # ------------------------------------------------------------------------------
536 __PACKAGE__->register_method(
537 method => "checkout",
538 api_name => "open-ils.circ.checkout",
541 @param authtoken The login session key
542 @param params A named hash of params including:
544 barcode If no copy is provided, the copy is retrieved via barcode
545 copyid If no copy or barcode is provide, the copy id will be use
546 patron The patron's id
547 noncat True if this is a circulation for a non-cataloted item
548 noncat_type The non-cataloged type id
549 noncat_circ_lib The location for the noncat circ.
550 precat The item has yet to be cataloged
551 dummy_title The temporary title of the pre-cataloded item
552 dummy_author The temporary authr of the pre-cataloded item
553 Default is the home org of the staff member
554 @return The SUCCESS event on success, any other event depending on the error
558 my( $self, $client, $authtoken, $params ) = @_;
561 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
562 my $key = $params->{permit_key};
564 # if this is a renewal, then the requestor does not have to
565 # have checkout privelages
566 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
567 ( $requestor, $evt ) = $U->checksesperm( $authtoken, 'COPY_CHECKOUT' ) unless $__isrenewal;
569 $logger->debug("REQUESTOR event: " . ref($requestor));
572 ( $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 = _apply_modified_due_date($ctx);
617 $evt = _commit_checkout_circ_object($ctx);
620 $evt = _update_checkout_copy($ctx);
624 ($holds, $evt) = _handle_related_holds($ctx);
628 $logger->debug("Checkin committing objects with session thread trace: ".$ctx->{session}->session_id);
629 $U->commit_db_session($ctx->{session});
630 my $record = $U->record_to_mvr($ctx->{title}) unless $ctx->{precat};
632 return OpenILS::Event->new('SUCCESS',
634 copy => $U->unflesh_copy($ctx->{copy}),
635 circ => $ctx->{circ},
637 holds_fulfilled => $holds,
642 sub _make_precat_copy {
643 my ( $requestor, $circlib, $params ) = @_;
645 my( $copy, undef ) = _find_copy_by_attr(%$params);
648 $logger->debug("Pre-cat copy already exists in checkout: ID=" . $copy->id);
649 return ($copy, undef);
652 $logger->debug("Creating a new precataloged copy in checkout with barcode " . $params->{barcode});
654 my $evt = OpenILS::Event->new(
655 'BAD_PARAMS', desc => "Dummy title or author not provided" )
656 unless ( $params->{dummy_title} and $params->{dummy_author} );
657 return (undef, $evt) if $evt;
659 $copy = Fieldmapper::asset::copy->new;
660 $copy->circ_lib($circlib);
661 $copy->creator($requestor->id);
662 $copy->editor($requestor->id);
663 $copy->barcode($params->{barcode});
664 $copy->call_number(-1); #special CN for precat materials
665 $copy->loan_duration(&PRECAT_LOAN_DURATION); # these two should come from constants
666 $copy->fine_level(&PRECAT_FINE_LEVEL);
668 $copy->dummy_title($params->{dummy_title});
669 $copy->dummy_author($params->{dummy_author});
671 my $id = $U->storagereq(
672 'open-ils.storage.direct.asset.copy.create', $copy );
673 return (undef, $U->DB_UPDATE_FAILED($copy)) unless $copy;
675 $logger->debug("Pre-cataloged copy successfully created");
676 return $U->fetch_copy($id);
680 sub _run_checkout_scripts {
686 my $runner = $ctx->{runner};
688 $runner->insert('result.durationLevel');
689 $runner->insert('result.durationRule');
690 $runner->insert('result.recurringFinesRule');
691 $runner->insert('result.recurringFinesLevel');
692 $runner->insert('result.maxFine');
694 $runner->load($scripts{circ_duration});
695 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
696 my $duration = $runner->retrieve('result.durationRule');
697 $logger->debug("Circ duration script yielded a duration rule of: $duration");
699 $runner->load($scripts{circ_recurring_fines});
700 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
701 my $recurring = $runner->retrieve('result.recurringFinesRule');
702 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
704 $runner->load($scripts{circ_max_fines});
705 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
706 my $max_fine = $runner->retrieve('result.maxFine');
707 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
709 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
711 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
713 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
716 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
717 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
718 $ctx->{duration_rule} = $duration;
719 $ctx->{recurring_fines_rule} = $recurring;
720 $ctx->{max_fine_rule} = $max_fine;
725 sub _build_checkout_circ_object {
729 my $circ = new Fieldmapper::action::circulation;
730 my $duration = $ctx->{duration_rule};
731 my $max = $ctx->{max_fine_rule};
732 my $recurring = $ctx->{recurring_fines_rule};
733 my $copy = $ctx->{copy};
734 my $patron = $ctx->{patron};
735 my $dur_level = $ctx->{duration_level};
736 my $rec_level = $ctx->{recurring_fines_level};
738 $circ->duration( $duration->shrt ) if ($dur_level == 1);
739 $circ->duration( $duration->normal ) if ($dur_level == 2);
740 $circ->duration( $duration->extended ) if ($dur_level == 3);
742 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
743 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
744 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
746 $circ->duration_rule( $duration->name );
747 $circ->recuring_fine_rule( $recurring->name );
748 $circ->max_fine_rule( $max->name );
749 $circ->max_fine( $max->amount );
751 $circ->fine_interval($recurring->recurance_interval);
752 $circ->renewal_remaining( $duration->max_renewals );
753 $circ->target_copy( $copy->id );
754 $circ->usr( $patron->id );
755 $circ->circ_lib( $ctx->{circ_lib} );
758 $logger->debug("Circ is a renewal. Setting renewal_remaining to " . $ctx->{renewal_remaining} );
759 $circ->opac_renewal(1);
760 $circ->renewal_remaining($ctx->{renewal_remaining});
761 $circ->circ_staff($ctx->{requestor}->id);
765 # if the user provided an overiding checkout time,
766 # (e.g. the checkout really happened several hours ago), then
767 # we apply that here. Does this need a perm??
768 if( my $ds = _create_date_stamp($ctx->{checkout_time}) ) {
769 $logger->debug("circ setting checkout_time to $ds");
770 $circ->xact_start($ds);
773 # if a patron is renewing, 'requestor' will be the patron
774 $circ->circ_staff($ctx->{requestor}->id );
775 _set_circ_due_date($circ);
776 $ctx->{circ} = $circ;
779 sub _apply_modified_due_date {
781 my $circ = $ctx->{circ};
783 if( $ctx->{due_date} ) {
785 my $evt = $U->check_perms(
786 $ctx->{requestor}->id, $ctx->{circ_lib}, 'CIRC_OVERRIDE_DUE_DATE');
789 my $ds = _create_date_stamp($ctx->{due_date});
790 $logger->debug("circ modifying due_date to $ds");
791 $circ->due_date($ds);
797 sub _create_date_stamp {
798 my $datestring = shift;
799 return undef unless $datestring;
800 $datestring = clense_ISO8601($datestring);
801 $logger->debug("circ created date stamp => $datestring");
805 sub _create_due_date {
806 my $duration = shift;
808 my ($sec,$min,$hour,$mday,$mon,$year) =
809 gmtime(OpenSRF::Utils->interval_to_seconds($duration) + int(time()));
810 $year += 1900; $mon += 1;
811 my $due_date = sprintf(
812 '%s-%0.2d-%0.2dT%s:%0.2d:%0.2d-00',
813 $year, $mon, $mday, $hour, $min, $sec);
817 sub _set_circ_due_date {
820 my $dd = _create_due_date($circ->duration);
821 $logger->debug("Checkout setting due date on circ to: $dd");
822 $circ->due_date($dd);
825 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
826 sub _update_checkout_copy {
829 my $copy = $ctx->{copy};
831 my $s = $U->copy_status_from_name('checked out');
832 $copy->status( $s->id ) if $s;
834 my $evt = $U->update_copy( session => $ctx->{session},
835 copy => $copy, editor => $ctx->{requestor}->id );
836 return (undef,$evt) if $evt;
841 # commits the circ object to the db then fleshes the circ with rules objects
842 sub _commit_checkout_circ_object {
845 my $circ = $ctx->{circ};
849 my $r = $ctx->{session}->request(
850 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
852 return $U->DB_UPDATE_FAILED($circ) unless $r;
854 $logger->debug("Created a new circ object in checkout: $r");
857 $circ->duration_rule($ctx->{duration_rule});
858 $circ->max_fine_rule($ctx->{max_fine_rule});
859 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
865 # sees if there are any holds that this copy
866 sub _handle_related_holds {
869 my $copy = $ctx->{copy};
870 my $patron = $ctx->{patron};
871 my $holds = $holdcode->fetch_related_holds($copy->id);
875 # XXX We should only fulfill one hold here...
876 # XXX If a hold was transited to the user who is checking out
877 # the item, we need to make sure that hold is what's grabbed
878 if(ref($holds) && @$holds) {
880 # for now, just sort by id to get what should be the oldest hold
881 $holds = [ sort { $a->id <=> $b->id } @$holds ];
882 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
885 my $hold = $holds->[0];
887 $logger->debug("Related hold found in checkout: " . $hold->id );
889 $hold->current_copy($copy->id); # just make sure it's set
890 # if the hold was never officially captured, capture it.
891 $hold->capture_time('now') unless $hold->capture_time;
892 $hold->fulfillment_time('now');
893 my $r = $ctx->{session}->request(
894 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
895 return (undef,$U->DB_UPDATE_FAILED( $hold )) unless $r;
896 push( @fulfilled, $hold->id );
900 return (\@fulfilled, undef);
903 sub _checkout_noncat {
904 my ( $key, $requestor, $patron, %params ) = @_;
905 my( $circ, $circlib, $evt );
908 $circlib = $params{noncat_circ_lib} || $requestor->home_ou;
910 return OpenILS::Event->new('CIRC_PERMIT_BAD_KEY')
911 unless _check_permit_key($key);
913 my $count = $params{noncat_count} || 1;
914 my $cotime = _create_date_stamp($params{checkout_time}) || "";
915 $logger->info("circ creating $count noncat circs with checkout time $cotime");
917 ( $circ, $evt ) = OpenILS::Application::Circ::NonCat::create_non_cat_circ(
918 $requestor->id, $patron->id, $circlib, $params{noncat_type}, $cotime );
922 return OpenILS::Event->new(
923 'SUCCESS', payload => { noncat_circ => $circ } );
927 __PACKAGE__->register_method(
928 method => "generic_receive",
929 api_name => "open-ils.circ.checkin",
932 Generic super-method for handling all copies
933 @param authtoken The login session key
934 @param params Hash of named parameters including:
935 barcode - The copy barcode
936 force - If true, copies in bad statuses will be checked in and give good statuses
941 sub generic_receive {
942 my( $self, $connection, $authtoken, $params ) = @_;
943 my( $ctx, $requestor, $evt );
945 ( $requestor, $evt ) = $U->checkses($authtoken) if $__isrenewal;
946 ( $requestor, $evt ) = $U->checksesperm(
947 $authtoken, 'COPY_CHECKIN' ) unless $__isrenewal;
950 # load up the circ objects
951 if( !( $ctx = $params->{_ctx}) ) {
952 ( $ctx, $evt ) = create_circ_ctx( %$params,
953 requestor => $requestor,
954 session => $U->start_db_session(),
956 fetch_copy_statuses => 1,
957 fetch_copy_locations => 1,
962 $ctx->{session} = $U->start_db_session() unless $ctx->{session};
963 $ctx->{authtoken} = $authtoken;
964 my $session = $ctx->{session};
966 my $copy = $ctx->{copy};
967 $U->unflesh_copy($copy);
968 return OpenILS::Event->new('COPY_NOT_FOUND') unless $copy;
970 $logger->info("Checkin copy called by user ".
971 $requestor->id." for copy ".$copy->id);
973 return $self->checkin_do_receive($connection, $ctx);
976 sub checkin_do_receive {
978 my( $self, $connection, $ctx ) = @_;
981 my $copy = $ctx->{copy};
982 my $session = $ctx->{session};
983 my $requestor = $ctx->{requestor};
984 my $change = 0; # did we actually do anything?
987 return $evt if ($evt = _checkin_check_copy_status($copy));
990 ($ctx->{circ}, $evt) = $U->fetch_open_circulation($copy->id);
991 return $evt if ($evt and $__isrenewal); # renewals require a circulation
993 ($ctx->{transit}) = $U->fetch_open_transit_by_copy($copy->id);
997 # There is an open circ on this item, close it out.
999 $evt = _checkin_handle_circ($ctx);
1000 return $evt if $evt;
1002 } elsif( $ctx->{transit} ) {
1004 # is this item currently in transit?
1006 $evt = $transcode->transit_receive( $copy, $requestor, $session );
1007 my $holdtrans = $evt->{holdtransit};
1008 ($ctx->{hold}) = $U->fetch_hold($holdtrans->hold) if $holdtrans;
1010 if( ! $U->event_equals($evt, 'SUCCESS') ) {
1012 # either an error occurred or a ROUTE_ITEM was generated and the
1013 # item must be forwarded on to its destination.
1014 return _checkin_flesh_event($ctx, $evt);
1020 # copy was received as a hold transit. Copy is at target lib
1021 # and hold transit is complete. We're done here...
1022 $U->commit_db_session($session);
1023 return _checkin_flesh_event($ctx, $evt);
1029 # ------------------------------------------------------------------------------
1030 # Circulations and transits are now closed where necessary. Now go on to see if
1031 # this copy can fulfill a hold or needs to be routed to a different location
1032 # ------------------------------------------------------------------------------
1035 # If it's a renewal, we're done
1037 $U->commit_db_session($session);
1038 return OpenILS::Event->new('SUCCESS');
1042 # Now, let's see if this copy is needed for a hold
1043 my ($hold) = $holdcode->find_local_hold( $session, $copy, $requestor );
1047 $ctx->{hold} = $hold;
1050 # Capture the hold with this copy
1051 return $evt if ($evt = _checkin_capture_hold($ctx));
1053 if( $hold->pickup_lib == $requestor->home_ou ) {
1055 # This hold was captured in the correct location
1056 $evt = OpenILS::Event->new('SUCCESS');
1060 # Hold needs to be picked up elsewhere. Build a hold
1061 # transit and route the item.
1062 return $evt if ($evt =_checkin_build_hold_transit($ctx));
1063 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $hold->pickup_lib);
1066 } else { # not needed for a hold
1068 if( $copy->circ_lib == $requestor->home_ou ) {
1070 # Copy is in the right place.
1071 $evt = OpenILS::Event->new('SUCCESS');
1073 # if the item happens to be a pre-cataloged item, send it
1074 # to cataloging and return the event
1075 my( $e, $c, $err ) = _checkin_handle_precat($ctx);
1076 return $err if $err;
1082 # Copy wants to go home. Transit it there.
1083 return $evt if ( $evt = _checkin_build_generic_copy_transit($ctx) );
1084 $evt = OpenILS::Event->new('ROUTE_ITEM', org => $copy->circ_lib);
1089 $logger->info("Copy checkin finished with event: ".$evt->{textcode});
1093 $evt = OpenILS::Event->new('NO_CHANGE');
1094 ($ctx->{hold}) = $U->fetch_open_hold_by_copy($copy->id)
1095 if ($copy->status == $U->copy_status_from_name('on holds shelf')->id);
1099 $U->commit_db_session($session);
1102 return _checkin_flesh_event($ctx, $evt);
1105 # returns (ITEM_NOT_CATALOGED, change_occurred, $error_event) where necessary
1106 sub _checkin_handle_precat {
1109 my $copy = $ctx->{copy};
1114 my $catstat = $U->copy_status_from_name('cataloging');
1116 if( $ctx->{precat} ) {
1118 $evt = OpenILS::Event->new('ITEM_NOT_CATALOGED');
1120 if( $copy->status != $catstat->id ) {
1121 $copy->status($catstat->id);
1123 return (undef, 0, $errevt) if (
1124 $errevt = $U->update_copy(
1126 editor => $ctx->{requestor}->id,
1127 session => $ctx->{session} ));
1133 return ($evt, $change, undef);
1137 sub _checkin_check_copy_status {
1139 my $stat = (ref($copy->status)) ? $copy->status->id : $copy->status;
1140 my $evt = OpenILS::Event->new('COPY_BAD_STATUS', payload => $copy );
1141 return $evt if ($stat == $U->copy_status_from_name('lost')->id);
1142 return $evt if ($stat == $U->copy_status_from_name('missing')->id);
1146 # Just gets the copy back home. Returns undef on success, event on error
1147 sub _checkin_build_generic_copy_transit {
1150 my $requestor = $ctx->{requestor};
1151 my $copy = $ctx->{copy};
1152 my $transit = Fieldmapper::action::transit_copy->new;
1153 my $session = $ctx->{session};
1155 $logger->activity("User ". $requestor->id ." creating a ".
1156 " new copy transit for copy ".$copy->id." to org ".$copy->circ_lib);
1158 $transit->source($requestor->home_ou);
1159 $transit->dest($copy->circ_lib);
1160 $transit->target_copy($copy->id);
1161 $transit->source_send_time('now');
1162 $transit->copy_status($copy->status);
1164 $logger->debug("Creating new copy_transit in DB");
1166 my $s = $session->request(
1167 "open-ils.storage.direct.action.transit_copy.create", $transit )->gather(1);
1168 return $U->DB_UPDATE_FAILED($transit) unless $s;
1170 $logger->info("Checkin copy successfully created new transit: $s");
1172 $copy->status($U->copy_status_from_name('in transit')->id );
1174 return $U->update_copy( copy => $copy,
1175 editor => $requestor->id, session => $session );
1180 # returns event on error, undef on success
1181 sub _checkin_build_hold_transit {
1184 my $copy = $ctx->{copy};
1185 my $hold = $ctx->{hold};
1186 my $trans = Fieldmapper::action::hold_transit_copy->new;
1188 $trans->hold($hold->id);
1189 $trans->source($ctx->{requestor}->home_ou);
1190 $trans->dest($hold->pickup_lib);
1191 $trans->source_send_time("now");
1192 $trans->target_copy($copy->id);
1193 $trans->copy_status($copy->status);
1195 my $id = $ctx->{session}->request(
1196 "open-ils.storage.direct.action.hold_transit_copy.create", $trans )->gather(1);
1197 return $U->DB_UPDATE_FAILED($trans) unless $id;
1199 $logger->info("Checkin copy successfully created hold transit: $id");
1201 $copy->status($U->copy_status_from_name('in transit')->id );
1202 return $U->update_copy( copy => $copy,
1203 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1206 # Returns event on error, undef on success
1207 sub _checkin_capture_hold {
1209 my $copy = $ctx->{copy};
1210 my $hold = $ctx->{hold};
1212 $logger->debug("Checkin copy capturing hold ".$hold->id);
1214 $hold->current_copy($copy->id);
1215 $hold->capture_time('now');
1217 my $stat = $ctx->{session}->request(
1218 "open-ils.storage.direct.action.hold_request.update", $hold)->gather(1);
1219 return $U->DB_UPDATE_FAILED($hold) unless $stat;
1221 $copy->status( $U->copy_status_from_name('on holds shelf')->id );
1223 return $U->update_copy( copy => $copy,
1224 editor => $ctx->{requestor}->id, session => $ctx->{session} );
1227 # fleshes an event with the relevant objects from the context
1228 sub _checkin_flesh_event {
1233 $payload->{copy} = $U->unflesh_copy($ctx->{copy});
1234 $payload->{record} = $U->record_to_mvr($ctx->{title}) if($ctx->{title} and !$ctx->{precat});
1235 $payload->{circ} = $ctx->{circ} if $ctx->{circ};
1236 $payload->{transit} = $ctx->{transit} if $ctx->{transit};
1237 $payload->{hold} = $ctx->{hold} if $ctx->{hold};
1239 $evt->{payload} = $payload;
1244 # Closes out the circulation, puts the copy into reshelving.
1245 # Voids any bills attached to this circ after the backdate time
1246 # if a backdate is provided
1247 sub _checkin_handle_circ {
1251 my $circ = $ctx->{circ};
1252 my $copy = $ctx->{copy};
1253 my $requestor = $ctx->{requestor};
1254 my $session = $ctx->{session};
1256 $logger->info("Handling circulation [".$circ->id."] found in checkin...");
1258 $ctx->{longoverdue} = 1 if ($circ->stop_fines =~ /longoverdue/io);
1259 $ctx->{claimsreturned} = 1 if ($circ->stop_fines =~ /claimsreturned/io);
1261 my ( $obt, $evt ) = $U->fetch_open_billable_transaction($circ->id);
1262 return $evt if $evt;
1264 $circ->stop_fines('CHECKIN');
1265 $circ->stop_fines('RENEW') if $__isrenewal;
1266 $circ->stop_fines('LOST') if($__islost);
1267 $circ->xact_finish('now') if($obt->balance_owed <= 0 and !$__islost);
1268 $circ->stop_fines_time('now');
1269 $circ->checkin_time('now');
1270 $circ->checkin_staff($requestor->id);
1272 if(my $backdate = $ctx->{backdate}) {
1273 return $evt if ($evt =
1274 _checkin_handle_backdate($backdate, $circ, $requestor, $session));
1277 $logger->info("Checkin copy setting status to 'reshelving' and committing...");
1278 $copy->status($U->copy_status_from_name('reshelving')->id);
1279 $evt = $U->update_copy( session => $session,
1280 copy => $copy, editor => $requestor->id );
1281 return $evt if $evt;
1283 $ctx->{session}->request(
1284 'open-ils.storage.direct.action.circulation.update', $circ )->gather(1);
1289 # returns event on error, undef on success
1290 # This voids all bills attached to the given circulation that occurred
1291 # after the backdate
1292 sub _checkin_handle_backdate {
1293 my( $backdate, $circ, $requestor, $session ) = @_;
1295 $logger->activity("User ".$requestor->id.
1296 " backdating checkin copy [".$circ->target_copy."] to date: $backdate");
1298 $circ->xact_finish($backdate);
1300 my $bills = $session->request( # XXX Verify this call is correct
1301 "open-ils.storage.direct.money.billing.search_where.atomic",
1302 billing_ts => { ">=" => $backdate }, "xact" => $circ->id )->gather(1);
1305 for my $bill (@$bills) {
1307 my $s = $session->request(
1308 "open-ils.storage.direct.money.billing.update", $bill)->gather(1);
1309 return $U->DB_UPDATE_FAILED($bill) unless $s;
1318 # ------------------------------------------------------------------------------
1320 __PACKAGE__->register_method(
1322 api_name => "open-ils.circ.renew",
1323 notes => <<" NOTES");
1324 PARAMS( authtoken, circ => circ_id );
1325 open-ils.circ.renew(login_session, circ_object);
1326 Renews the provided circulation. login_session is the requestor of the
1327 renewal and if the logged in user is not the same as circ->usr, then
1328 the logged in user must have RENEW_CIRC permissions.
1332 my( $self, $client, $authtoken, $params ) = @_;
1335 my ( $requestor, $patron, $ctx, $evt, $circ, $copy );
1338 # if requesting a renewal for someone else, you must have
1340 ( $requestor, $patron, $evt ) = $U->checkses_requestor(
1341 $authtoken, $params->{patron}, 'RENEW_CIRC' );
1342 if($evt) { $__isrenewal = 0; return $evt; }
1345 # fetch and build the circulation environment
1346 ( $ctx, $evt ) = create_circ_ctx( %$params,
1348 requestor => $requestor,
1351 fetch_patron_circ_summary => 1,
1352 fetch_copy_statuses => 1,
1353 fetch_copy_locations => 1,
1355 if($evt) { $__isrenewal = 0; return $evt; }
1356 $params->{_ctx} = $ctx;
1358 # make sure they have some renewals left and make sure the circulation exists
1359 ($circ, $evt) = _check_renewal_remaining($ctx);
1360 if($evt) { $__isrenewal = 0; return $evt; }
1361 $ctx->{old_circ} = $circ;
1362 my $renewals = $circ->renewal_remaining - 1;
1364 # run the renew permit script
1365 $evt = _run_renew_scripts($ctx);
1366 if($evt) { $__isrenewal = 0; return $evt; }
1369 #$ctx->{patron} = $ctx->{patron}->id;
1370 $evt = $self->generic_receive($client, $authtoken, $ctx );
1371 #{ barcode => $params->{barcode}, patron => $params->{patron}} );
1373 if( !$U->event_equals($evt, 'SUCCESS') ) {
1374 $__isrenewal = 0; return $evt;
1377 # re-fetch the context since objects have changed in the checkin
1378 ( $ctx, $evt ) = create_circ_ctx( %$params,
1380 requestor => $requestor,
1383 fetch_patron_circ_summary => 1,
1384 fetch_copy_statuses => 1,
1385 fetch_copy_locations => 1,
1387 if($evt) { $__isrenewal = 0; return $evt; }
1388 $params->{_ctx} = $ctx;
1389 $ctx->{renewal_remaining} = $renewals;
1391 # run the circ permit scripts
1392 $evt = $self->permit_circ( $client, $authtoken, $params );
1393 if( $U->event_equals($evt, 'ITEM_NOT_CATALOGED')) {
1396 if(!$U->event_equals($evt, 'SUCCESS')) {
1397 if($evt) { $__isrenewal = 0; return $evt; }
1400 $params->{permit_key} = $evt->{payload};
1403 # checkout the item again
1404 $evt = $self->checkout($client, $authtoken, $params );
1410 sub _check_renewal_remaining {
1413 my( $circ, $evt ) = $U->fetch_open_circulation($ctx->{copy}->id);
1414 return (undef, $evt) if $evt;
1415 $evt = OpenILS::Event->new(
1416 'MAX_RENEWALS_REACHED') if $circ->renewal_remaining < 1;
1417 return ($circ, $evt);
1420 sub _run_renew_scripts {
1422 my $runner = $ctx->{runner};
1425 $runner->load($scripts{circ_permit_renew});
1426 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Renew Script Died: $@");
1427 my $evtname = $runner->retrieve('result.event');
1428 $logger->activity("circ_permit_renew for user ".$ctx->{patron}->id." returned event: $evtname");
1430 return OpenILS::Event->new($evtname) if $evtname ne 'SUCCESS';