1 package OpenILS::Application::Circ::Circulate;
2 use base 'OpenSRF::Application';
3 use strict; use warnings;
4 use OpenSRF::EX qw(:try);
7 use OpenSRF::Utils::Logger qw(:logger);
8 use OpenILS::Utils::ScriptRunner;
9 use OpenILS::Application::AppUtils;
10 use OpenILS::Application::Circ::Holds;
11 $Data::Dumper::Indent = 0;
12 my $apputils = "OpenILS::Application::AppUtils";
14 my $holdcode = "OpenILS::Application::Circ::Holds";
16 my %scripts; # - circulation script filenames
17 my $script_libs; # - any additional script libraries
18 my %cache; # - db objects cache
19 my %contexts; # - Script runner contexts
21 # ------------------------------------------------------------------------------
22 # Load the circ script from the config
23 # ------------------------------------------------------------------------------
27 my $conf = OpenSRF::Utils::SettingsClient->new;
28 my @pfx2 = ( "apps", "open-ils.circ","app_settings" );
29 my @pfx = ( @pfx2, "scripts" );
31 my $p = $conf->config_value( @pfx, 'circ_permit_patron' );
32 my $c = $conf->config_value( @pfx, 'circ_permit_copy' );
33 my $d = $conf->config_value( @pfx, 'circ_duration' );
34 my $f = $conf->config_value( @pfx, 'circ_recurring_fines' );
35 my $m = $conf->config_value( @pfx, 'circ_max_fines' );
36 my $pr = $conf->config_value( @pfx, 'renew_permit' );
37 my $ph = $conf->config_value( @pfx, 'hold_permit' );
38 my $lb = $conf->config_value( @pfx2, 'script_path' );
40 $logger->error( "Missing circ script(s)" )
41 unless( $p and $c and $d and $f and $m and $pr and $ph );
43 $scripts{circ_permit_patron} = $p;
44 $scripts{circ_permit_copy} = $c;
45 $scripts{circ_duration} = $d;
46 $scripts{circ_recurring_fines}= $f;
47 $scripts{circ_max_fines} = $m;
48 $scripts{circ_renew_permit} = $pr;
49 $scripts{hold_permit} = $ph;
51 $lb = [ $lb ] unless ref($lb);
54 $logger->debug("Loaded rules scripts for circ: " .
55 "circ permit patron: $p, circ permit copy: $c, ".
56 "circ duration :$d , circ recurring fines : $f, " .
57 "circ max fines : $m, circ renew permit : $pr, permit hold: $ph");
61 # ------------------------------------------------------------------------------
62 # Loads the necessary circ objects and pushes them into the script environment
63 # Returns ( $data, $evt ). if $evt is defined, then an
64 # unexpedted event occurred and should be dealt with / returned to the caller
65 # ------------------------------------------------------------------------------
72 $evt = _ctx_add_patron_objects($ctx, %params);
75 if( ($params{copy} or $params{copyid} or $params{barcode}) and !$params{noncat} ) {
76 $evt = _ctx_add_copy_objects($ctx, %params);
80 _doctor_patron_object($ctx) if $ctx->{patron};
81 _doctor_copy_object($ctx) if $ctx->{copy};
82 _doctor_circ_objects($ctx);
83 _build_circ_script_runner($ctx);
84 _add_script_runner_methods( $ctx );
89 sub _ctx_add_patron_objects {
90 my( $ctx, %params) = @_;
92 $ctx->{patron} = $params{patron};
94 if(!defined($cache{patron_standings})) {
95 $cache{patron_standings} = $apputils->fetch_patron_standings();
96 $cache{group_tree} = $apputils->fetch_permission_group_tree();
99 $ctx->{patron_standings} = $cache{patron_standings};
100 $ctx->{group_tree} = $cache{group_tree};
102 $ctx->{patron_circ_summary} =
103 $apputils->fetch_patron_circ_summary($ctx->{patron}->id)
104 if $params{fetch_patron_circsummary};
110 sub _ctx_add_copy_objects {
111 my($ctx, %params) = @_;
114 $cache{copy_statuses} = $apputils->fetch_copy_statuses
115 if( $params{fetch_copy_statuses} and !defined($cache{copy_statuses}) );
117 $cache{copy_locations} = $apputils->fetch_copy_locations
118 if( $params{fetch_copy_locations} and !defined($cache{copy_locations}));
120 $ctx->{copy_statuses} = $cache{copy_statuses};
121 $ctx->{copy_locations} = $cache{copy_locations};
123 my $copy = $params{copy} if $params{copy};
128 $apputils->fetch_copy($params{copyid}) if $params{copyid};
133 $apputils->fetch_copy_by_barcode( $params{barcode} ) if $params{barcode};
138 $ctx->{copy} = $copy;
140 ( $ctx->{title}, $evt ) = $apputils->fetch_record_by_copy( $ctx->{copy}->id );
147 # ------------------------------------------------------------------------------
148 # Fleshes parts of the patron object
149 # ------------------------------------------------------------------------------
150 sub _doctor_copy_object {
153 my $copy = $ctx->{copy};
155 # set the copy status to a status name
156 $copy->status( _get_copy_status(
157 $copy, $ctx->{copy_statuses} ) ) if $copy;
159 # set the copy location to the location object
160 $copy->location( _get_copy_location(
161 $copy, $ctx->{copy_locations} ) ) if $copy;
163 $copy->circ_lib( $U->fetch_org_unit($copy->circ_lib) );
167 # ------------------------------------------------------------------------------
168 # Fleshes parts of the copy object
169 # ------------------------------------------------------------------------------
170 sub _doctor_patron_object {
172 my $patron = $ctx->{patron};
174 # push the standing object into the patron
175 if(ref($ctx->{patron_standings})) {
176 for my $s (@{$ctx->{patron_standings}}) {
177 $patron->standing($s) if ( $s->id eq $ctx->{patron}->standing );
181 # set the patron ptofile to the profile name
182 $patron->profile( _get_patron_profile(
183 $patron, $ctx->{group_tree} ) ) if $ctx->{group_tree};
187 $apputils->fetch_org_unit( $patron->home_ou ) ) if $patron;
191 # recurse and find the patron profile name from the tree
192 # another option would be to grab the groups for the patron
193 # and cycle through those until the "profile" group has been found
194 sub _get_patron_profile {
195 my( $patron, $group_tree ) = @_;
196 return $group_tree if ($group_tree->id eq $patron->profile);
197 return undef unless ($group_tree->children);
199 for my $child (@{$group_tree->children}) {
200 my $ret = _get_patron_profile( $patron, $child );
206 sub _get_copy_status {
207 my( $copy, $cstatus ) = @_;
209 for my $status (@$cstatus) {
210 $s = $status if( $status->id eq $copy->status )
212 $logger->debug("Retrieving copy status: " . $s->name) if $s;
216 sub _get_copy_location {
217 my( $copy, $locations ) = @_;
219 for my $loc (@$locations) {
220 $l = $loc if $loc->id eq $copy->location;
222 $logger->debug("Retrieving copy location: " . $l->name ) if $l;
227 # ------------------------------------------------------------------------------
228 # Constructs and shoves data into the script environment
229 # ------------------------------------------------------------------------------
230 sub _build_circ_script_runner {
233 $logger->debug("Loading script environment for circulation");
236 if( $runner = $contexts{$ctx->{type}} ) {
237 $runner->refresh_context;
239 $runner = OpenILS::Utils::ScriptRunner->new unless $runner;
240 $contexts{type} = $runner;
244 $logger->debug("Loading circ script lib path $_");
245 $runner->add_path( $_ );
248 $runner->insert( 'environment.patron', $ctx->{patron}, 1);
249 $runner->insert( 'environment.title', $ctx->{title}, 1);
250 $runner->insert( 'environment.copy', $ctx->{copy}, 1);
253 $runner->insert( 'result', {} );
254 $runner->insert( 'result.event', 'SUCCESS' );
256 $runner->insert('environment.isRenewal', 1) if $ctx->{renew};
257 $runner->insert('environment.isNonCat', 1) if $ctx->{noncat};
258 $runner->insert('environment.nonCatType', $ctx->{noncat_type}) if $ctx->{noncat};
260 if(ref($ctx->{patron_circ_summary})) {
261 $runner->insert( 'environment.patronItemsOut', $ctx->{patron_circ_summary}->[0], 1 );
262 $runner->insert( 'environment.patronFines', $ctx->{patron_circ_summary}->[1], 1 );
265 $ctx->{runner} = $runner;
270 sub _add_script_runner_methods {
272 my $runner = $ctx->{runner};
276 # allows a script to fetch a hold that is currently targeting the
278 $runner->insert_method( 'environment.copy', '__OILS_FUNC_fetch_hold', sub {
280 my $hold = $holdcode->fetch_related_holds($ctx->{copy}->id);
281 $hold = undef unless $hold;
282 $runner->insert( $key, $hold, 1 );
288 # ------------------------------------------------------------------------------
290 __PACKAGE__->register_method(
291 method => "permit_circ",
292 api_name => "open-ils.circ.checkout.permit",
294 Determines if the given checkout can occur
295 @param authtoken The login session key
296 @param params A trailing hash of named params including
297 barcode : The copy barcode,
298 patron : The patron the checkout is occurring for,
299 renew : true or false - whether or not this is a renewal
300 @return The event that occurred during the permit check.
301 If all is well, the SUCCESS event is returned
305 my( $self, $client, $authtoken, $params ) = @_;
307 my ( $requestor, $patron, $ctx, $evt );
309 # check permisson of the requestor
310 ( $requestor, $patron, $evt ) =
311 $apputils->checkses_requestor(
312 $authtoken, $params->{patron}, 'VIEW_PERMIT_CHECKOUT' );
315 # fetch and build the circulation environment
316 ( $ctx, $evt ) = create_circ_ctx( %$params,
318 requestor => $requestor,
320 fetch_patron_circ_summary => 1,
321 fetch_copy_statuses => 1,
322 fetch_copy_locations => 1,
326 return _run_permit_scripts($ctx);
330 # Runs the patron and copy permit scripts
331 # if this is a non-cat circulation, the copy permit script
333 sub _run_permit_scripts {
336 my $runner = $ctx->{runner};
337 my $patronid = $ctx->{patron}->id;
338 my $barcode = ($ctx->{copy}) ? $ctx->{copy}->barcode : undef;
340 $runner->load($scripts{circ_permit_patron});
341 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Patron Script Died: $@");
342 my $evtname = $runner->retrieve('result.event');
343 $logger->activity("circ_permit_patron for user $patronid returned event: $evtname");
345 return OpenILS::Event->new($evtname)
346 if ( $ctx->{noncat} or $evtname ne 'SUCCESS' );
348 $runner->load($scripts{circ_permit_copy});
349 $runner->run or throw OpenSRF::EX::ERROR ("Circ Permit Copy Script Died: $@");
350 $evtname = $runner->retrieve('result.event');
351 $logger->activity("circ_permit_patron for user $patronid ".
352 "and copy $barcode returned event: $evtname");
354 return OpenILS::Event->new($evtname);
359 # ------------------------------------------------------------------------------
361 __PACKAGE__->register_method(
362 method => "checkout",
363 api_name => "open-ils.circ.checkout",
366 @param authtoken The login session key
367 @param params A named hash of params including:
369 barcode If no copy is provided, the copy is retrieved via barcode
370 copyid If no copy or barcode is provide, the copy id will be use
371 patron The patron's id
372 noncat True if this is a circulation for a non-cataloted item
373 noncat_type The non-cataloged type id
374 noncat_circ_lib The location for the noncat circ.
375 Default is the home org of the staff member
376 @return The SUCCESS event on success, any other event depending on the error
380 my( $self, $client, $authtoken, $params ) = @_;
382 my ( $requestor, $patron, $ctx, $evt, $circ );
384 # check permisson of the requestor
385 ( $requestor, $patron, $evt ) =
386 $apputils->checkses_requestor(
387 $authtoken, $params->{patron}, 'COPY_CHECKOUT' );
390 return _checkout_noncat( $requestor, $patron, %$params ) if $params->{noncat};
392 my $session = $U->start_db_session();
394 # fetch and build the circulation environment
395 ( $ctx, $evt ) = create_circ_ctx( %$params,
397 requestor => $requestor,
400 fetch_patron_circ_summary => 1,
401 fetch_copy_statuses => 1,
402 fetch_copy_locations => 1,
406 $ctx->{circ_lib} = (defined($params->{circ_lib})) ?
407 $params->{circ_lib} : $requestor->home_ou;
409 $evt = _run_checkout_scripts($ctx);
412 _build_checkout_circ_object($ctx);
414 $evt = _commit_checkout_circ_object($ctx);
417 _update_checkout_copy($ctx);
419 $evt = _handle_related_holds($ctx);
422 #$U->commit_db_session($session);
424 return OpenILS::Event->new('SUCCESS',
426 copy => $ctx->{copy},
427 circ => $ctx->{circ},
428 record => $U->record_to_mvr($ctx->{title}),
433 sub _run_checkout_scripts {
438 my $runner = $ctx->{runner};
440 $runner->insert('result.durationLevel');
441 $runner->insert('result.durationRule');
442 $runner->insert('result.recurringFinesRule');
443 $runner->insert('result.recurringFinesLevel');
444 $runner->insert('result.maxFine');
446 $runner->load($scripts{circ_duration});
447 $runner->run or throw OpenSRF::EX::ERROR ("Circ Duration Script Died: $@");
448 my $duration = $runner->retrieve('result.durationRule');
449 $logger->debug("Circ duration script yielded a duration rule of: $duration");
451 $runner->load($scripts{circ_recurring_fines});
452 $runner->run or throw OpenSRF::EX::ERROR ("Circ Recurring Fines Script Died: $@");
453 my $recurring = $runner->retrieve('result.recurringFinesRule');
454 $logger->debug("Circ recurring fines script yielded a rule of: $recurring");
456 $runner->load($scripts{circ_max_fines});
457 $runner->run or throw OpenSRF::EX::ERROR ("Circ Max Fine Script Died: $@");
458 my $max_fine = $runner->retrieve('result.maxFine');
459 $logger->debug("Circ max_fine fines script yielded a rule of: $max_fine");
461 ($duration, $evt) = $U->fetch_circ_duration_by_name($duration);
463 ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring);
465 ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine);
468 $ctx->{duration_level} = $runner->retrieve('result.durationLevel');
469 $ctx->{recurring_fines_level} = $runner->retrieve('result.recurringFinesLevel');
470 $ctx->{duration_rule} = $duration;
471 $ctx->{recurring_fines_rule} = $recurring;
472 $ctx->{max_fine_rule} = $max_fine;
477 sub _build_checkout_circ_object {
480 my $circ = new Fieldmapper::action::circulation;
481 my $duration = $ctx->{duration_rule};
482 my $max = $ctx->{max_fine_rule};
483 my $recurring = $ctx->{recurring_fines_rule};
484 my $copy = $ctx->{copy};
485 my $patron = $ctx->{patron};
486 my $dur_level = $ctx->{duration_level};
487 my $rec_level = $ctx->{recurring_fines_level};
489 $circ->duration( $duration->shrt ) if ($dur_level == 1);
490 $circ->duration( $duration->normal ) if ($dur_level == 2);
491 $circ->duration( $duration->extended ) if ($dur_level == 3);
493 $circ->recuring_fine( $recurring->low ) if ($rec_level =~ /low/io);
494 $circ->recuring_fine( $recurring->normal ) if ($rec_level =~ /normal/io);
495 $circ->recuring_fine( $recurring->high ) if ($rec_level =~ /high/io);
497 $circ->duration_rule( $duration->name );
498 $circ->recuring_fine_rule( $recurring->name );
499 $circ->max_fine_rule( $max->name );
500 $circ->max_fine( $max->amount );
502 $circ->fine_interval($recurring->recurance_interval);
503 $circ->renewal_remaining( $duration->max_renewals );
504 $circ->target_copy( $copy->id );
505 $circ->usr( $patron->id );
506 $circ->circ_lib( $ctx->{circ_lib} );
508 if( $ctx->{renew} ) {
509 $circ->opac_renewal(1); # XXX different for different types ?????
511 #$circ->renewal_remaining($numrenews - 1); # XXX
512 $circ->circ_staff($ctx->{patron}->id);
515 $circ->circ_staff( $ctx->{requestor}->id );
518 _set_circ_due_date($circ);
519 $ctx->{circ} = $circ;
522 sub _set_circ_due_date {
525 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
526 gmtime(OpenSRF::Utils->interval_to_seconds($circ->duration) + int(time()));
528 $year += 1900; $mon += 1;
529 my $due_date = sprintf(
530 '%s-%0.2d-%0.2dT%s:%0.2d:%0.s2-00',
531 $year, $mon, $mday, $hour, $min, $sec);
533 $logger->debug("Checkout setting due date on circ to: $due_date");
534 $circ->due_date($due_date);
537 # Sets the editor, edit_date, un-fleshes the copy, and updates the copy in the DB
538 sub _update_checkout_copy {
540 my $copy = $ctx->{copy};
542 $copy->status( $copy->status->id );
543 $copy->editor( $ctx->{requestor}->id );
544 $copy->edit_date( 'now' );
545 $copy->location( $copy->location->id );
546 $copy->circ_lib( $copy->circ_lib->id );
548 $logger->debug("Updating editor info on copy in checkout: " . $copy->id );
549 $ctx->{session}->request(
550 'open-ils.storage.direct.asset.copy.update', $copy )->gather(1);
553 # commits the circ object to the db then fleshes the circ with rules objects
554 sub _commit_checkout_circ_object {
557 my $circ = $ctx->{circ};
559 my $r = $ctx->{session}->request(
560 "open-ils.storage.direct.action.circulation.create", $circ )->gather(1);
562 return $U->DB_UPDATE_FAILED($circ) unless $r;
564 $logger->debug("Created a new circ object in checkout: $r");
567 $circ->duration_rule($ctx->{duration_rule});
568 $circ->max_fine_rule($ctx->{max_fine_rule});
569 $circ->recuring_fine_rule($ctx->{recurring_fines_rule});
575 # sees if there are any holds that this copy
576 sub _handle_related_holds {
579 my $copy = $ctx->{copy};
580 my $patron = $ctx->{patron};
581 my $holds = $holdcode->fetch_related_holds($copy->id);
583 if(ref($holds) && @$holds) {
585 # for now, just sort by id to get what should be the oldest hold
586 $holds = [ sort { $a->id <=> $b->id } @$holds ];
587 $holds = [ grep { $_->usr eq $patron->id } @$holds ];
590 my $hold = $holds->[0];
592 $logger->debug("Related hold found in checkout: " . $hold->id );
594 $hold->fulfillment_time('now');
595 my $r = $ctx->{session}->request(
596 "open-ils.storage.direct.action.hold_request.update", $hold )->gather(1);
597 return $U->DB_UPDATE_FAILED( $hold ) unless $r;
605 sub _checkout_noncat {
606 my ( $requestor, $patron, %params ) = @_;
607 my $circlib = $params{noncat_circ_lib} || $requestor->home_ou;
609 OpenILS::Application::Circ::NonCat::create_non_cat_circ(
610 $requestor->id, $patron->id, $circlib, $params{noncat_type} );
612 return OpenILS::Event->new('SUCCESS');
616 # ------------------------------------------------------------------------------
618 __PACKAGE__->register_method(
620 api_name => "open-ils.circ.checkin",
621 notes => <<" NOTES");
622 PARAMS( authtoken, barcode => bc )
623 Checks in based on barcode
624 Returns an event object whose payload contains the record, circ, and copy
625 If the item needs to be routed, the event is a ROUTE_COPY event
626 with an additional 'route_to' variable set on the event
630 my( $self, $client, $authtoken, $params ) = @_;
633 # ------------------------------------------------------------------------------
635 __PACKAGE__->register_method(
637 api_name => "open-ils.circ.renew_",
638 notes => <<" NOTES");
639 PARAMS( authtoken, circ => circ_id );
640 open-ils.circ.renew(login_session, circ_object);
641 Renews the provided circulation. login_session is the requestor of the
642 renewal and if the logged in user is not the same as circ->usr, then
643 the logged in user must have RENEW_CIRC permissions.
647 my( $self, $client, $authtoken, $params ) = @_;