1 package OpenILS::Application::Circ;
2 use OpenILS::Application;
3 use base qw/OpenILS::Application/;
4 use strict; use warnings;
6 use OpenILS::Application::Circ::Circulate;
7 use OpenILS::Application::Circ::Survey;
8 use OpenILS::Application::Circ::StatCat;
9 use OpenILS::Application::Circ::Holds;
10 use OpenILS::Application::Circ::HoldNotify;
11 use OpenILS::Application::Circ::CircNotify;
12 use OpenILS::Application::Circ::CreditCard;
13 use OpenILS::Application::Circ::Money;
14 use OpenILS::Application::Circ::NonCat;
15 use OpenILS::Application::Circ::CopyLocations;
16 use OpenILS::Application::Circ::CircCommon;
19 use DateTime::Format::ISO8601;
21 use OpenILS::Application::AppUtils;
23 use OpenILS::Utils::DateTime qw/:datetime/;
24 use OpenSRF::AppSession;
25 use OpenILS::Utils::ModsParser;
27 use OpenSRF::EX qw(:try);
28 use OpenSRF::Utils::Logger qw(:logger);
29 use OpenILS::Utils::Fieldmapper;
30 use OpenILS::Utils::CStoreEditor q/:funcs/;
31 use OpenILS::Const qw/:const/;
32 use OpenSRF::Utils::SettingsClient;
33 use OpenILS::Application::Cat::AssetCommon;
35 my $apputils = "OpenILS::Application::AppUtils";
38 my $holdcode = "OpenILS::Application::Circ::Holds";
40 # ------------------------------------------------------------------------
41 # Top level Circ package;
42 # ------------------------------------------------------------------------
46 OpenILS::Application::Circ::Circulate->initialize();
50 __PACKAGE__->register_method(
51 method => 'retrieve_circ',
53 api_name => 'open-ils.circ.retrieve',
55 Retrieve a circ object by id
56 @param authtoken Login session key
57 @pararm circid The id of the circ object
58 @param all_circ Returns an action.all_circulation_slim object instead
59 of an action.circulation object to pick up aged circs.
64 my( $s, $c, $a, $i, $all_circ ) = @_;
65 my $e = new_editor(authtoken => $a);
66 return $e->event unless $e->checkauth;
67 my $method = $all_circ ?
68 'retrieve_action_all_circulation_slim' :
69 'retrieve_action_circulation';
70 my $circ = $e->$method($i) or return $e->event;
71 if( $e->requestor->id ne ($circ->usr || '') ) {
72 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
78 __PACKAGE__->register_method(
79 method => 'fetch_circ_mods',
80 api_name => 'open-ils.circ.circ_modifier.retrieve.all');
82 my($self, $conn, $args) = @_;
83 my $mods = new_editor()->retrieve_all_config_circ_modifier;
84 return [ map {$_->code} @$mods ] unless $$args{full};
88 __PACKAGE__->register_method(
89 method => 'ranged_billing_types',
90 api_name => 'open-ils.circ.billing_type.ranged.retrieve.all');
92 sub ranged_billing_types {
93 my($self, $conn, $auth, $org_id, $depth) = @_;
94 my $e = new_editor(authtoken => $auth);
95 return $e->event unless $e->checkauth;
96 return $e->event unless $e->allowed('VIEW_BILLING_TYPE', $org_id);
97 return $e->search_config_billing_type(
98 {owner => $U->get_org_full_path($org_id, $depth)});
103 # ------------------------------------------------------------------------
104 # Returns an array of {circ, record} hashes checked out by the user.
105 # ------------------------------------------------------------------------
106 __PACKAGE__->register_method(
107 method => "checkouts_by_user",
108 api_name => "open-ils.circ.actor.user.checked_out",
110 NOTES => <<" NOTES");
111 Returns a list of open circulations as a pile of objects. Each object
112 contains the relevant copy, circ, and record
115 sub checkouts_by_user {
116 my($self, $client, $auth, $user_id) = @_;
118 my $e = new_editor(authtoken=>$auth);
119 return $e->event unless $e->checkauth;
121 my $circ_ids = $e->search_action_circulation(
123 checkin_time => undef,
125 {stop_fines => undef},
126 {stop_fines => ['MAXFINES','LONGOVERDUE']}
132 for my $id (@$circ_ids) {
133 my $circ = $e->retrieve_action_circulation([
137 circ => ['target_copy'],
138 acp => ['call_number'],
144 # un-flesh for consistency
145 my $c = $circ->target_copy;
146 $circ->target_copy($c->id);
148 my $cn = $c->call_number;
149 $c->call_number($cn->id);
157 record => $U->record_to_mvr($t)
167 __PACKAGE__->register_method(
168 method => "checkouts_by_user_slim",
169 api_name => "open-ils.circ.actor.user.checked_out.slim",
170 NOTES => <<" NOTES");
171 Returns a list of open circulation objects
175 sub checkouts_by_user_slim {
176 my( $self, $client, $user_session, $user_id ) = @_;
178 my( $requestor, $target, $copy, $record, $evt );
180 ( $requestor, $target, $evt ) =
181 $apputils->checkses_requestor( $user_session, $user_id, 'VIEW_CIRCULATIONS');
184 $logger->debug( 'User ' . $requestor->id .
185 " retrieving checked out items for user " . $target->id );
187 # XXX Make the call correct..
188 return $apputils->simplereq(
190 "open-ils.cstore.direct.action.open_circulation.search.atomic",
191 { usr => $target->id, checkin_time => undef } );
192 # { usr => $target->id } );
196 __PACKAGE__->register_method(
197 method => "checkouts_by_user_opac",
198 api_name => "open-ils.circ.actor.user.checked_out.opac",);
201 sub checkouts_by_user_opac {
202 my( $self, $client, $auth, $user_id ) = @_;
204 my $e = new_editor( authtoken => $auth );
205 return $e->event unless $e->checkauth;
206 $user_id ||= $e->requestor->id;
207 return $e->event unless
208 my $patron = $e->retrieve_actor_user($user_id);
211 my $search = {usr => $user_id, stop_fines => undef};
213 if( $user_id ne $e->requestor->id ) {
214 $data = $e->search_action_circulation(
215 $search, {checkperm=>1, permorg=>$patron->home_ou})
219 $data = $e->search_action_circulation($search);
226 __PACKAGE__->register_method(
227 method => "title_from_transaction",
228 api_name => "open-ils.circ.circ_transaction.find_title",
229 NOTES => <<" NOTES");
230 Returns a mods object for the title that is linked to from the
231 copy from the hold that created the given transaction
234 sub title_from_transaction {
235 my( $self, $client, $login_session, $transactionid ) = @_;
237 my( $user, $circ, $title, $evt );
239 ( $user, $evt ) = $apputils->checkses( $login_session );
242 ( $circ, $evt ) = $apputils->fetch_circulation($transactionid);
245 ($title, $evt) = $apputils->fetch_record_by_copy($circ->target_copy);
248 return $apputils->record_to_mvr($title);
251 __PACKAGE__->register_method(
252 method => "staff_age_to_lost",
253 api_name => "open-ils.circ.circulation.age_to_lost",
256 This fires a circ.staff_age_to_lost Action-Trigger event against all
257 overdue circulations in scope of the specified context library and
258 user profile, which effectively marks the associated items as Lost.
259 This is likely to be done at the end of a semester in an academic
262 @param args : circ_lib, user_profile
266 sub staff_age_to_lost {
267 my( $self, $conn, $auth, $args ) = @_;
268 my $e = new_editor(authtoken=>$auth);
269 return $e->event unless $e->checkauth;
270 return $e->event unless $e->allowed('SET_CIRC_LOST', $args->{'circ_lib'});
272 my $orgs = $U->get_org_descendants($args->{'circ_lib'});
273 my $profiles = $U->fetch_permission_group_descendants($args->{'user_profile'});
275 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
277 my $method = 'open-ils.trigger.passive.event.autocreate.batch';
278 my $hook = 'circ.staff_age_to_lost';
279 my $context_org = 'circ_lib';
280 my $opt_granularity = undef;
282 "checkin_time" => undef,
283 "due_date" => { "<" => "now" },
285 { "stop_fines" => ["MAXFINES", "LONGOVERDUE"] }, # FIXME: CLAIMSRETURNED also?
286 { "stop_fines" => undef }
290 "select" => {"au" => ["id"]},
293 "profile" => $profiles,
294 "id" => { "=" => {"+circ" => "usr"} }
298 "select" => {"aou" => ["id"]},
302 {"id" => { "=" => {"+circ" => "circ_lib"} }},
309 my $req_timeout = 10800;
310 my $chunk_size = 100;
313 my $req = $ses->request($method, $hook, $context_org, $filter, $opt_granularity);
314 my @event_ids; my @chunked_ids;
315 while (my $resp = $req->recv(timeout => $req_timeout)) {
316 push(@event_ids, $resp->content);
317 push(@chunked_ids, $resp->content);
318 if (scalar(@chunked_ids) > $chunk_size) {
319 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
323 if (scalar(@chunked_ids) > 0) {
324 $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
328 $logger->info("staff_age_to_lost: created ".scalar(@event_ids)." events for circ.staff_age_to_lost");
329 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>scalar(@event_ids)});
330 } elsif($req->complete) {
331 $logger->info("staff_age_to_lost: no events to create for circ.staff_age_to_lost");
332 $conn->respond_complete({'total_progress'=>$progress-1,'created'=>0});
334 $logger->warn("staff_age_to_lost: timeout occurred during event creation for circ.staff_age_to_lost");
335 $conn->respond_complete({'total_progress'=>$progress-1,'error'=>'timeout'});
342 __PACKAGE__->register_method(
343 method => "new_set_circ_lost",
344 api_name => "open-ils.circ.circulation.set_lost",
346 Sets the copy and related open circulation to lost
348 @param args : barcode
353 # ---------------------------------------------------------------------
354 # Sets a circulation to lost. updates copy status to lost
355 # applies copy and/or prcoessing fees depending on org settings
356 # ---------------------------------------------------------------------
357 sub new_set_circ_lost {
358 my( $self, $conn, $auth, $args ) = @_;
360 my $e = new_editor(authtoken=>$auth, xact=>1);
361 return $e->die_event unless $e->checkauth;
363 my $copy = $e->search_asset_copy({barcode=>$$args{barcode}, deleted=>'f'})->[0]
364 or return $e->die_event;
366 my $evt = OpenILS::Application::Cat::AssetCommon->set_item_lost($e, $copy->id);
373 __PACKAGE__->register_method(
374 method => "update_copy_inventory",
375 api_name => "open-ils.circ.circulation.update_copy_inventory");
377 sub update_copy_inventory {
378 my( $self, $conn, $auth, $args ) = @_;
379 my $e = new_editor(authtoken=>$auth);
380 return [0, 0, $e->die_event] unless $e->checkauth;
382 my ($success,$failure) = (0,0);
383 my $spname = "aci_update_savepoint";
385 my $copies = $$args{copy_list};
386 foreach my $copyid (@$copies) {
387 my $aci = Fieldmapper::asset::copy_inventory->new;
388 $aci->inventory_date('now');
389 $aci->inventory_workstation($e->requestor->wsid);
391 $e->set_savepoint($spname);
392 if ($e->create_asset_copy_inventory($aci)) {
396 $e->rollback_savepoint($spname);
398 $e->release_savepoint($spname);
406 return [ $success, $failure ];
409 __PACKAGE__->register_method(
410 method => "set_circ_claims_returned",
411 api_name => "open-ils.circ.circulation.set_claims_returned",
413 desc => q/Sets the circ for a given item as claims returned
414 If a backdate is provided, overdue fines will be voided
415 back to the backdate/,
417 {desc => 'Authentication token', type => 'string'},
418 {desc => 'Arguments, including "barcode" and optional "backdate"', type => 'object'}
420 return => {desc => q/1 on success, failure event on error, and
421 PATRON_EXCEEDS_CLAIMS_RETURN_COUNT if the patron exceeds the
422 configured claims return maximum/}
426 __PACKAGE__->register_method(
427 method => "set_circ_claims_returned",
428 api_name => "open-ils.circ.circulation.set_claims_returned.override",
430 desc => q/This adds support for overrideing the configured max
431 claims returned amount.
432 @see open-ils.circ.circulation.set_claims_returned./,
436 sub set_circ_claims_returned {
437 my( $self, $conn, $auth, $args, $oargs ) = @_;
439 my $e = new_editor(authtoken=>$auth, xact=>1);
440 return $e->die_event unless $e->checkauth;
442 $oargs = { all => 1 } unless defined $oargs;
444 my $barcode = $$args{barcode};
445 my $backdate = $$args{backdate};
447 my $copy = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'})->[0]
448 or return $e->die_event;
450 my $circ = $e->search_action_circulation(
451 {checkin_time => undef, target_copy => $copy->id})->[0]
452 or return $e->die_event;
454 $backdate = $circ->due_date if $$args{use_due_date};
456 $logger->info("marking circ for item $barcode as claims returned".
457 (($backdate) ? " with backdate $backdate" : ''));
459 my $patron = $e->retrieve_actor_user($circ->usr);
460 my $max_count = $U->ou_ancestor_setting_value(
461 $circ->circ_lib, 'circ.max_patron_claim_return_count', $e);
463 # If the patron has too instances of many claims returned,
464 # require an override to continue. A configured max of
465 # 0 means all attempts require an override
466 if(defined $max_count and $patron->claims_returned_count >= $max_count) {
468 if($self->api_name =~ /override/ && ($oargs->{all} || grep { $_ eq 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT' } @{$oargs->{events}})) {
470 # see if we're allowed to override
471 return $e->die_event unless
472 $e->allowed('SET_CIRC_CLAIMS_RETURNED.override', $circ->circ_lib);
476 # exit early and return the max claims return event
478 return OpenILS::Event->new(
479 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT',
481 patron_count => $patron->claims_returned_count,
482 max_count => $max_count
488 $e->allowed('SET_CIRC_CLAIMS_RETURNED', $circ->circ_lib)
489 or return $e->die_event;
491 $circ->stop_fines(OILS_STOP_FINES_CLAIMSRETURNED);
492 $circ->stop_fines_time('now') unless $circ->stop_fines_time;
495 $backdate = clean_ISO8601($backdate);
497 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($circ->due_date));
498 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($backdate);
499 $backdate = $new_date->ymd . 'T' . $original_date->strftime('%T%z');
501 # clean it up once again; need a : in the timezone offset. E.g. -06:00 not -0600
502 $backdate = clean_ISO8601($backdate);
504 # make it look like the circ stopped at the cliams returned time
505 $circ->stop_fines_time($backdate);
506 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate, note => 'System: OVERDUE REVERSED FOR CLAIMS-RETURNED', force_zero => 1});
510 $e->update_action_circulation($circ) or return $e->die_event;
512 # see if there is a configured post-claims-return copy status
513 if(my $stat = $U->ou_ancestor_setting_value($circ->circ_lib, 'circ.claim_return.copy_status')) {
514 $copy->status($stat);
515 $copy->edit_date('now');
516 $copy->editor($e->requestor->id);
517 $e->update_asset_copy($copy) or return $e->die_event;
520 # Check if the copy circ lib wants lost fees voided on claims
522 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_on_claimsreturned', $e))) {
523 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
534 # Check if the copy circ lib wants lost processing fees voided on
536 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_proc_fee_on_claimsreturned', $e))) {
537 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
548 # Check if the copy circ lib wants longoverdue fees voided on claims
550 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_on_claimsreturned', $e))) {
551 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
562 # Check if the copy circ lib wants longoverdue processing fees voided on
564 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_proc_fee_on_claimsreturned', $e))) {
565 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
576 # Now that all data has been munged, do a no-op update of
577 # the patron to force a change of the last_xact_id value.
578 $e->update_actor_user($e->retrieve_actor_user($circ->usr))
579 or return $e->die_event;
586 __PACKAGE__->register_method(
587 method => "post_checkin_backdate_circ",
588 api_name => "open-ils.circ.post_checkin_backdate",
590 desc => q/Back-date an already checked in circulation/,
592 {desc => 'Authentication token', type => 'string'},
593 {desc => 'Circ ID', type => 'number'},
594 {desc => 'ISO8601 backdate', type => 'string'},
596 return => {desc => q/1 on success, failure event on error/}
600 __PACKAGE__->register_method(
601 method => "post_checkin_backdate_circ",
602 api_name => "open-ils.circ.post_checkin_backdate.batch",
605 desc => q/@see open-ils.circ.post_checkin_backdate. Batch mode/,
607 {desc => 'Authentication token', type => 'string'},
608 {desc => 'List of Circ ID', type => 'array'},
609 {desc => 'ISO8601 backdate', type => 'string'},
611 return => {desc => q/Set of: 1 on success, failure event on error/}
616 sub post_checkin_backdate_circ {
617 my( $self, $conn, $auth, $circ_id, $backdate ) = @_;
618 my $e = new_editor(authtoken=>$auth);
619 return $e->die_event unless $e->checkauth;
620 if($self->api_name =~ /batch/) {
621 foreach my $c (@$circ_id) {
622 $conn->respond(post_checkin_backdate_circ_impl($e, $c, $backdate));
625 $conn->respond_complete(post_checkin_backdate_circ_impl($e, $circ_id, $backdate));
633 sub post_checkin_backdate_circ_impl {
634 my($e, $circ_id, $backdate) = @_;
638 my $circ = $e->retrieve_action_circulation($circ_id)
639 or return $e->die_event;
641 # anyone with checkin perms can backdate (more restrictive?)
642 return $e->die_event unless $e->allowed('COPY_CHECKIN', $circ->circ_lib);
644 # don't allow back-dating an open circulation
645 return OpenILS::Event->new('BAD_PARAMS') unless
646 $backdate and $circ->checkin_time;
648 # update the checkin and stop_fines times to reflect the new backdate
649 $circ->stop_fines_time(clean_ISO8601($backdate));
650 $circ->checkin_time(clean_ISO8601($backdate));
651 $e->update_action_circulation($circ) or return $e->die_event;
653 # now void the overdues "erased" by the back-dating
654 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate});
657 # If the circ was closed before and the balance owned !=0, re-open the transaction
658 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
667 __PACKAGE__->register_method (
668 method => 'set_circ_due_date',
669 api_name => 'open-ils.circ.circulation.due_date.update',
671 Updates the due_date on the given circ
673 @param circid The id of the circ to update
674 @param date The timestamp of the new due date
678 sub set_circ_due_date {
679 my( $self, $conn, $auth, $circ_id, $date ) = @_;
681 my $e = new_editor(xact=>1, authtoken=>$auth);
682 return $e->die_event unless $e->checkauth;
683 my $circ = $e->retrieve_action_circulation($circ_id)
684 or return $e->die_event;
686 return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
687 $date = clean_ISO8601($date);
689 if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
690 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($circ->due_date));
691 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
693 # since the new date may be coming in as UTC, convert it
694 # to the same time zone as the original due date so that
695 # ->ymd is more likely to yield the expected results
696 $new_date->set_time_zone($original_date->time_zone());
697 $date = clean_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
700 $circ->due_date($date);
701 $e->update_action_circulation($circ) or return $e->die_event;
708 __PACKAGE__->register_method(
709 method => "create_in_house_use",
710 api_name => 'open-ils.circ.in_house_use.create',
712 Creates an in-house use action.
713 @param $authtoken The login session key
714 @param params A hash of params including
715 'location' The org unit id where the in-house use occurs
716 'copyid' The copy in question
717 'count' The number of in-house uses to apply to this copy
718 @return An array of id's representing the id's of the newly created
719 in-house use objects or an event on an error
722 __PACKAGE__->register_method(
723 method => "create_in_house_use",
724 api_name => 'open-ils.circ.non_cat_in_house_use.create',
728 sub create_in_house_use {
729 my( $self, $client, $auth, $params ) = @_;
732 my $org = $params->{location};
733 my $copyid = $params->{copyid};
734 my $count = $params->{count} || 1;
735 my $nc_type = $params->{non_cat_type};
736 my $use_time = $params->{use_time} || 'now';
738 my $e = new_editor(xact=>1,authtoken=>$auth);
739 return $e->event unless $e->checkauth;
740 return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
742 my $non_cat = 1 if $self->api_name =~ /non_cat/;
746 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
748 $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
754 if( $use_time ne 'now' ) {
755 $use_time = clean_ISO8601($use_time);
756 $logger->debug("in_house_use setting use time to $use_time");
767 $ihu = Fieldmapper::action::non_cat_in_house_use->new;
768 $ihu->item_type($nc_type);
769 $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
770 $cmeth = "create_action_non_cat_in_house_use";
773 $ihu = Fieldmapper::action::in_house_use->new;
775 $method = 'open-ils.storage.direct.action.in_house_use.create';
776 $cmeth = "create_action_in_house_use";
779 $ihu->staff($e->requestor->id);
780 $ihu->workstation($e->requestor->wsid);
781 $ihu->org_unit($org);
782 $ihu->use_time($use_time);
784 $ihu = $e->$cmeth($ihu) or return $e->event;
785 push( @ids, $ihu->id );
796 __PACKAGE__->register_method(
797 method => "view_circs",
798 api_name => "open-ils.circ.copy_checkout_history.retrieve",
800 Retrieves the last X circs for a given copy
801 @param authtoken The login session key
802 @param copyid The copy to check
803 @param count How far to go back in the item history
804 @return An array of circ ids
807 # ----------------------------------------------------------------------
808 # Returns $count most recent circs. If count exceeds the configured
809 # max, use the configured max instead
810 # ----------------------------------------------------------------------
812 my( $self, $client, $authtoken, $copyid, $count ) = @_;
814 my $e = new_editor(authtoken => $authtoken);
815 return $e->event unless $e->checkauth;
817 my $copy = $e->retrieve_asset_copy([
820 flesh_fields => {acp => ['call_number']}
822 ]) or return $e->event;
824 return $e->event unless $e->allowed(
825 'VIEW_COPY_CHECKOUT_HISTORY',
826 ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ?
827 $copy->circ_lib : $copy->call_number->owning_lib);
829 my $max_history = $U->ou_ancestor_setting_value(
830 $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
832 if(defined $max_history) {
833 $count = $max_history unless defined $count and $count < $max_history;
835 $count = 4 unless defined $count;
838 return $e->search_action_all_circulation_slim([
839 {target_copy => $copyid},
840 {limit => $count, order_by => { aacs => "xact_start DESC" }}
845 __PACKAGE__->register_method(
846 method => "circ_count",
847 api_name => "open-ils.circ.circulation.count",
849 Returns the number of times the item has circulated
850 @param copyid The copy to check
854 my( $self, $client, $copyid ) = @_;
856 my $count = new_editor()->json_query({
865 where => {'+circbyyr' => {copy => $copyid}}
877 __PACKAGE__->register_method(
878 method => 'fetch_notes',
880 api_name => 'open-ils.circ.copy_note.retrieve.all',
882 Returns an array of copy note objects.
883 @param args A named hash of parameters including:
884 authtoken : Required if viewing non-public notes
885 itemid : The id of the item whose notes we want to retrieve
886 pub : True if all the caller wants are public notes
887 @return An array of note objects
890 __PACKAGE__->register_method(
891 method => 'fetch_notes',
892 api_name => 'open-ils.circ.call_number_note.retrieve.all',
893 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
895 __PACKAGE__->register_method(
896 method => 'fetch_notes',
897 api_name => 'open-ils.circ.title_note.retrieve.all',
898 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
901 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
903 my( $self, $connection, $args ) = @_;
905 my $id = $$args{itemid};
906 my $authtoken = $$args{authtoken};
909 if( $self->api_name =~ /copy/ ) {
911 return $U->cstorereq(
912 'open-ils.cstore.direct.asset.copy_note.search.atomic',
913 { owning_copy => $id, pub => 't' } );
915 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
917 return $U->cstorereq(
918 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
921 } elsif( $self->api_name =~ /call_number/ ) {
923 return $U->cstorereq(
924 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
925 { call_number => $id, pub => 't' } );
927 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
929 return $U->cstorereq(
930 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
933 } elsif( $self->api_name =~ /title/ ) {
935 return $U->cstorereq(
936 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
937 { record => $id, pub => 't' } );
939 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
941 return $U->cstorereq(
942 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
949 __PACKAGE__->register_method(
950 method => 'has_notes',
951 api_name => 'open-ils.circ.copy.has_notes');
952 __PACKAGE__->register_method(
953 method => 'has_notes',
954 api_name => 'open-ils.circ.call_number.has_notes');
955 __PACKAGE__->register_method(
956 method => 'has_notes',
957 api_name => 'open-ils.circ.title.has_notes');
961 my( $self, $conn, $authtoken, $id ) = @_;
962 my $editor = new_editor(authtoken => $authtoken);
963 return $editor->event unless $editor->checkauth;
965 my $n = $editor->search_asset_copy_note(
966 {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
968 $n = $editor->search_asset_call_number_note(
969 {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
971 $n = $editor->search_biblio_record_note(
972 {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
979 __PACKAGE__->register_method(
980 method => 'create_copy_note',
981 api_name => 'open-ils.circ.copy_note.create',
983 Creates a new copy note
984 @param authtoken The login session key
985 @param note The note object to create
986 @return The id of the new note object
989 sub create_copy_note {
990 my( $self, $connection, $authtoken, $note ) = @_;
992 my $e = new_editor(xact=>1, authtoken=>$authtoken);
993 return $e->event unless $e->checkauth;
994 my $copy = $e->retrieve_asset_copy(
998 flesh_fields => { 'acp' => ['call_number'] }
1003 return $e->event unless
1004 $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
1006 $note->create_date('now');
1007 $note->creator($e->requestor->id);
1008 $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
1011 $e->create_asset_copy_note($note) or return $e->event;
1017 __PACKAGE__->register_method(
1018 method => 'delete_copy_note',
1019 api_name => 'open-ils.circ.copy_note.delete',
1021 Deletes an existing copy note
1022 @param authtoken The login session key
1023 @param noteid The id of the note to delete
1024 @return 1 on success - Event otherwise.
1026 sub delete_copy_note {
1027 my( $self, $conn, $authtoken, $noteid ) = @_;
1029 my $e = new_editor(xact=>1, authtoken=>$authtoken);
1030 return $e->die_event unless $e->checkauth;
1032 my $note = $e->retrieve_asset_copy_note([
1036 'acpn' => [ 'owning_copy' ],
1037 'acp' => [ 'call_number' ],
1040 ]) or return $e->die_event;
1042 if( $note->creator ne $e->requestor->id ) {
1043 return $e->die_event unless
1044 $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
1047 $e->delete_asset_copy_note($note) or return $e->die_event;
1052 __PACKAGE__->register_method(
1053 method => 'fetch_copy_tags',
1055 api_name => 'open-ils.circ.copy_tags.retrieve',
1057 Returns an array of publicly-visible copy tag objects.
1058 @param args A named hash of parameters including:
1059 copy_id : The id of the item whose notes we want to retrieve
1060 tag_type : Type of copy tags to retrieve, e.g., 'bookplate' (optional)
1061 scope : top of org subtree whose copy tags we want to see
1062 depth : how far down to look for copy tags (optional)
1063 @return An array of copy tag objects
1065 __PACKAGE__->register_method(
1066 method => 'fetch_copy_tags',
1068 api_name => 'open-ils.circ.copy_tags.retrieve.staff',
1070 Returns an array of all copy tag objects.
1071 @param args A named hash of parameters including:
1072 authtoken : Required to view non-public notes
1073 copy_id : The id of the item whose notes we want to retrieve (optional)
1074 tag_type : Type of copy tags to retrieve, e.g., 'bookplate'
1075 scope : top of org subtree whose copy tags we want to see
1076 depth : how far down to look for copy tags (optional)
1077 @return An array of copy tag objects
1080 sub fetch_copy_tags {
1081 my ($self, $conn, $args) = @_;
1083 my $org = $args->{scope};
1084 my $depth = $args->{depth};
1088 if ($self->api_name =~ /\.staff/) {
1089 my $authtoken = $args->{authtoken};
1090 return new OpenILS::Event("BAD_PARAMS", "desc" => "authtoken required") unless defined $authtoken;
1091 $e = new_editor(authtoken => $args->{authtoken});
1092 return $e->event unless $e->checkauth;
1093 return $e->event unless $e->allowed('STAFF_LOGIN', $org);
1096 $filter->{pub} = 't';
1098 $filter->{tag_type} = $args->{tag_type} if exists($args->{tag_type});
1099 $filter->{'+acptcm'} = {
1100 copy => $args->{copy_id}
1103 # filter by owner of copy tag and depth
1104 $filter->{owner} = {
1106 select => {aou => [{
1108 transform => 'actor.org_unit_descendants',
1109 result_field => 'id',
1110 (defined($depth) ? ( params => [$depth] ) : ()),
1113 where => {id => $org}
1117 return $e->search_asset_copy_tag([$filter, {
1118 join => { acptcm => {} },
1120 flesh_fields => { acpt => ['tag_type'] }
1125 __PACKAGE__->register_method(
1126 method => 'age_hold_rules',
1127 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
1130 sub age_hold_rules {
1131 my( $self, $conn ) = @_;
1132 return new_editor()->retrieve_all_config_rules_age_hold_protect();
1137 __PACKAGE__->register_method(
1138 method => 'copy_details_barcode',
1140 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
1141 sub copy_details_barcode {
1142 my( $self, $conn, $auth, $barcode ) = @_;
1143 my $e = new_editor();
1144 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
1145 return $e->event unless $cid;
1146 return copy_details( $self, $conn, $auth, $cid );
1150 __PACKAGE__->register_method(
1151 method => 'copy_details',
1152 api_name => 'open-ils.circ.copy_details.retrieve');
1155 my( $self, $conn, $auth, $copy_id ) = @_;
1156 my $e = new_editor(authtoken=>$auth);
1157 return $e->event unless $e->checkauth;
1159 my $flesh = { flesh => 1 };
1161 my $copy = $e->retrieve_asset_copy(
1167 acp => ['call_number','parts','peer_record_maps','floating'],
1168 acn => ['record','prefix','suffix','label_class']
1171 ]) or return $e->event;
1174 # De-flesh the copy for backwards compatibility
1176 my $vol = $copy->call_number;
1178 $copy->call_number($vol->id);
1179 my $record = $vol->record;
1181 $vol->record($record->id);
1182 $mvr = $U->record_to_mvr($record);
1187 my $hold = $e->search_action_hold_request(
1189 current_copy => $copy_id,
1190 capture_time => { "!=" => undef },
1191 fulfillment_time => undef,
1192 cancel_time => undef,
1196 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1198 my $transit = $e->search_action_transit_copy(
1199 { target_copy => $copy_id, dest_recv_time => undef, cancel_time => undef } )->[0];
1201 # find the most recent circulation for the requested copy,
1202 # be it active, completed, or aged.
1203 my $circ = $e->search_action_all_circulation_slim([
1204 { target_copy => $copy_id },
1210 'checkin_workstation',
1213 'recurring_fine_rule'
1216 order_by => { aacs => 'xact_start desc' },
1224 transit => $transit,
1234 __PACKAGE__->register_method(
1235 method => 'mark_item',
1236 api_name => 'open-ils.circ.mark_item_damaged',
1238 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1239 @param authtoken The login session key
1240 @param copy_id The ID of the copy to mark as damaged
1241 @return 1 on success - Event otherwise.
1244 __PACKAGE__->register_method(
1245 method => 'mark_item',
1246 api_name => 'open-ils.circ.mark_item_missing',
1248 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1249 @param authtoken The login session key
1250 @param copy_id The ID of the copy to mark as missing
1251 @return 1 on success - Event otherwise.
1254 __PACKAGE__->register_method(
1255 method => 'mark_item',
1256 api_name => 'open-ils.circ.mark_item_bindery',
1258 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1259 @param authtoken The login session key
1260 @param copy_id The ID of the copy to mark as bindery
1261 @return 1 on success - Event otherwise.
1264 __PACKAGE__->register_method(
1265 method => 'mark_item',
1266 api_name => 'open-ils.circ.mark_item_on_order',
1268 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1269 @param authtoken The login session key
1270 @param copy_id The ID of the copy to mark as on order
1271 @return 1 on success - Event otherwise.
1274 __PACKAGE__->register_method(
1275 method => 'mark_item',
1276 api_name => 'open-ils.circ.mark_item_ill',
1278 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1279 @param authtoken The login session key
1280 @param copy_id The ID of the copy to mark as inter-library loan
1281 @return 1 on success - Event otherwise.
1284 __PACKAGE__->register_method(
1285 method => 'mark_item',
1286 api_name => 'open-ils.circ.mark_item_cataloging',
1288 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1289 @param authtoken The login session key
1290 @param copy_id The ID of the copy to mark as cataloging
1291 @return 1 on success - Event otherwise.
1294 __PACKAGE__->register_method(
1295 method => 'mark_item',
1296 api_name => 'open-ils.circ.mark_item_reserves',
1298 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1299 @param authtoken The login session key
1300 @param copy_id The ID of the copy to mark as reserves
1301 @return 1 on success - Event otherwise.
1304 __PACKAGE__->register_method(
1305 method => 'mark_item',
1306 api_name => 'open-ils.circ.mark_item_discard',
1308 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1309 @param authtoken The login session key
1310 @param copy_id The ID of the copy to mark as discard
1311 @return 1 on success - Event otherwise.
1316 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1319 my $e = new_editor(authtoken=>$auth);
1320 return $e->die_event unless $e->checkauth;
1321 my $copy = $e->retrieve_asset_copy([
1323 {flesh => 1, flesh_fields => {'acp' => ['call_number','status']}}])
1324 or return $e->die_event;
1327 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1328 $copy->circ_lib : $copy->call_number->owning_lib;
1330 my $evt; # For later.
1331 my $perm = 'MARK_ITEM_MISSING';
1332 my $stat = OILS_COPY_STATUS_MISSING;
1334 if( $self->api_name =~ /damaged/ ) {
1335 $perm = 'MARK_ITEM_DAMAGED';
1336 $stat = OILS_COPY_STATUS_DAMAGED;
1337 } elsif ( $self->api_name =~ /bindery/ ) {
1338 $perm = 'MARK_ITEM_BINDERY';
1339 $stat = OILS_COPY_STATUS_BINDERY;
1340 } elsif ( $self->api_name =~ /on_order/ ) {
1341 $perm = 'MARK_ITEM_ON_ORDER';
1342 $stat = OILS_COPY_STATUS_ON_ORDER;
1343 } elsif ( $self->api_name =~ /ill/ ) {
1344 $perm = 'MARK_ITEM_ILL';
1345 $stat = OILS_COPY_STATUS_ILL;
1346 } elsif ( $self->api_name =~ /cataloging/ ) {
1347 $perm = 'MARK_ITEM_CATALOGING';
1348 $stat = OILS_COPY_STATUS_CATALOGING;
1349 } elsif ( $self->api_name =~ /reserves/ ) {
1350 $perm = 'MARK_ITEM_RESERVES';
1351 $stat = OILS_COPY_STATUS_RESERVES;
1352 } elsif ( $self->api_name =~ /discard/ ) {
1353 $perm = 'MARK_ITEM_DISCARD';
1354 $stat = OILS_COPY_STATUS_DISCARD;
1357 # caller may proceed if either perm is allowed
1358 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1360 # Copy status checks.
1361 if ($copy->status->id() == OILS_COPY_STATUS_CHECKED_OUT) {
1362 # Checked out items should not be marked missing.
1363 # Otherwise, attempt a checkin before a status change.
1364 if ($stat ne OILS_COPY_STATUS_MISSING && $args->{handle_checkin}) {
1365 $evt = try_checkin($auth, $copy_id);
1367 $evt = OpenILS::Event->new('ITEM_TO_MARK_CHECKED_OUT');
1369 } elsif ($copy->status->id() == OILS_COPY_STATUS_IN_TRANSIT) {
1370 # Items in transit need to have the transit aborted before being marked.
1371 if ($args->{handle_transit}) {
1372 $evt = try_abort_transit($auth, $copy_id);
1374 $evt = OpenILS::Event->new('ITEM_TO_MARK_IN_TRANSIT');
1376 } elsif ($U->is_true($copy->status->restrict_copy_delete()) && $self->api_name =~ /discard/) {
1377 # Items with restrict_copy_delete status require the
1378 # COPY_DELETE_WARNING.override permission to be marked for
1380 if ($args->{handle_copy_delete_warning}) {
1381 $evt = $e->event unless $e->allowed(['COPY_DELETE_WARNING.override'], $owning_lib);
1383 $evt = OpenILS::Event->new('COPY_DELETE_WARNING');
1386 return $evt if $evt;
1388 # Retrieving holds for later use.
1389 my $holds = $e->search_action_hold_request([
1391 current_copy => $copy->id,
1392 fulfillment_time => undef,
1393 cancel_time => undef,
1395 {flesh=>1, flesh_fields=>{ahr=>['eligible_copies']}}
1398 # Throw event if attempting to mark discard the only copy to fill a hold.
1399 if ($self->api_name =~ /discard/) {
1400 if (!$args->{handle_last_hold_copy}) {
1401 for my $hold (@$holds) {
1402 my $eligible = $hold->eligible_copies();
1403 if (!defined($eligible) || scalar(@{$eligible}) < 2) {
1404 $evt = OpenILS::Event->new('ITEM_TO_MARK_LAST_HOLD_COPY');
1410 return $evt if $evt;
1412 # Things below here require a transaction and there is nothing left to interfere with it.
1415 # Handle extra mark damaged charges, etc.
1416 if ($self->api_name =~ /damaged/) {
1417 $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1418 return $evt if $evt;
1422 $copy->status($stat);
1423 $copy->edit_date('now');
1424 $copy->editor($e->requestor->id);
1426 $e->update_asset_copy($copy) or return $e->die_event;
1430 if( $self->api_name =~ /damaged/ ) {
1431 # now that we've committed the changes, create related A/T events
1432 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1433 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1436 $logger->debug("resetting holds that target the marked copy");
1437 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1443 my($auth, $copy_id) = @_;
1445 my $checkin = $U->simplereq(
1447 'open-ils.circ.checkin.override',
1449 copy_id => $copy_id,
1453 if(ref $checkin ne 'ARRAY') { $checkin = [$checkin]; }
1455 my $evt_code = $checkin->[0]->{textcode};
1456 $logger->info("try_checkin() received event: $evt_code");
1458 if($evt_code eq 'SUCCESS' || $evt_code eq 'NO_CHANGE') {
1459 $logger->info('try_checkin() successful checkin');
1462 $logger->warn('try_checkin() un-successful checkin');
1467 sub try_abort_transit {
1468 my ($auth, $copy_id) = @_;
1470 my $abort = $U->simplereq(
1472 'open-ils.circ.transit.abort',
1473 $auth, {copyid => $copy_id}
1475 # Above returns 1 or an event.
1476 return $abort if (ref $abort);
1480 sub handle_mark_damaged {
1481 my($e, $copy, $owning_lib, $args) = @_;
1483 my $apply = $args->{apply_fines} || '';
1484 return undef if $apply eq 'noapply';
1486 my $new_amount = $args->{override_amount};
1487 my $new_btype = $args->{override_btype};
1488 my $new_note = $args->{override_note};
1490 # grab the last circulation
1491 my $circ = $e->search_action_circulation([
1492 { target_copy => $copy->id},
1494 order_by => {circ => "xact_start DESC"},
1496 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1500 return undef unless $circ;
1502 my $charge_price = $U->ou_ancestor_setting_value(
1503 $owning_lib, 'circ.charge_on_damaged', $e);
1505 my $proc_fee = $U->ou_ancestor_setting_value(
1506 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1508 my $void_overdue = $U->ou_ancestor_setting_value(
1509 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1511 return undef unless $charge_price or $proc_fee;
1513 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1514 my $total = $copy_price + $proc_fee;
1518 if($new_amount and $new_btype) {
1520 # Allow staff to override the amount to charge for a damaged item
1521 # Consider the case where the item is only partially damaged
1522 # This value is meant to take the place of the item price and
1523 # optional processing fee.
1525 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1526 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1527 return $evt if $evt;
1531 if($charge_price and $copy_price) {
1532 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1533 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1534 return $evt if $evt;
1538 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1539 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1540 return $evt if $evt;
1544 # the assumption is that you would not void the overdues unless you
1545 # were also charging for the item and/or applying a processing fee
1547 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {note => 'System: OVERDUE REVERSED FOR DAMAGE CHARGE'});
1548 return $evt if $evt;
1551 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1552 return $evt if $evt;
1554 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1555 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1557 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1558 return $evt2 if $evt2;
1561 return OpenILS::Event->new('DAMAGE_CHARGE',
1572 # ----------------------------------------------------------------------
1573 __PACKAGE__->register_method(
1574 method => 'mark_item_missing_pieces',
1575 api_name => 'open-ils.circ.mark_item_missing_pieces',
1577 Changes the status of a copy to "damaged" or to a custom status based on the
1578 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1580 @param authtoken The login session key
1581 @param copy_id The ID of the copy to mark as damaged
1582 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1586 sub mark_item_missing_pieces {
1587 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1588 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1589 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1591 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1592 return $e2->die_event unless $e2->checkauth;
1595 my $copy = $e2->retrieve_asset_copy([
1597 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1598 or return $e2->die_event;
1601 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1602 $copy->circ_lib : $copy->call_number->owning_lib;
1604 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1606 #### grab the last circulation
1607 my $circ = $e2->search_action_circulation([
1608 { target_copy => $copy->id},
1610 order_by => {circ => "xact_start DESC"}
1615 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1617 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1620 my $holds = $e2->search_action_hold_request(
1622 current_copy => $copy->id,
1623 fulfillment_time => undef,
1624 cancel_time => undef,
1628 $logger->debug("resetting holds that target the marked copy");
1629 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1632 if (! $e2->commit) {
1633 return $e2->die_event;
1636 my $e = new_editor(authtoken=>$auth, xact =>1);
1637 return $e->die_event unless $e->checkauth;
1639 if (! $circ->checkin_time) { # if circ active, attempt renew
1640 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1641 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1642 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1643 $circ = $res->[0]->{payload}{'circ'};
1644 $circ->target_copy( $copy->id );
1645 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1647 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1652 'copy_id'=>$circ->target_copy,
1653 'patron_id'=>$circ->usr,
1654 'skip_deposit_fee'=>1,
1655 'skip_rental_fee'=>1
1658 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1660 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1661 $e, $copy, $e->requestor, 1 );
1663 if ($hold) { # needed for hold? then due now
1665 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1666 my $due_date = DateTime->now(time_zone => 'local');
1667 $co_params->{'due_date'} = clean_ISO8601( $due_date->strftime('%FT%T%z') );
1669 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1673 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1674 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1675 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1676 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1677 $circ = $res->[0]->{payload}{'circ'};
1679 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1685 ### Update the item status
1687 my $custom_stat = $U->ou_ancestor_setting_value(
1688 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1689 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1691 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1692 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1694 $copy->status($stat);
1695 $copy->edit_date('now');
1696 $copy->editor($e->requestor->id);
1698 $e->update_asset_copy($copy) or return $e->die_event;
1702 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1703 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1705 return OpenILS::Event->new('SUCCESS',
1709 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1710 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1715 return $e->die_event;
1723 # ----------------------------------------------------------------------
1724 __PACKAGE__->register_method(
1725 method => 'magic_fetch',
1726 api_name => 'open-ils.agent.fetch'
1729 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1732 my( $self, $conn, $auth, $args ) = @_;
1733 my $e = new_editor( authtoken => $auth );
1734 return $e->event unless $e->checkauth;
1736 my $hint = $$args{hint};
1737 my $id = $$args{id};
1739 # Is the call allowed to fetch this type of object?
1740 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1742 # Find the class the implements the given hint
1743 my ($class) = grep {
1744 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1746 $class =~ s/Fieldmapper:://og;
1747 $class =~ s/::/_/og;
1748 my $method = "retrieve_$class";
1750 my $obj = $e->$method($id) or return $e->event;
1753 # ----------------------------------------------------------------------
1756 __PACKAGE__->register_method(
1757 method => "fleshed_circ_retrieve",
1759 api_name => "open-ils.circ.fleshed.retrieve",);
1761 sub fleshed_circ_retrieve {
1762 my( $self, $client, $id ) = @_;
1763 my $e = new_editor();
1764 my $circ = $e->retrieve_action_circulation(
1770 circ => [ qw/ target_copy / ],
1771 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1772 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1773 acn => [ qw/ record / ],
1777 ) or return $e->event;
1779 my $copy = $circ->target_copy;
1780 my $vol = $copy->call_number;
1781 my $rec = $circ->target_copy->call_number->record;
1783 $vol->record($rec->id);
1784 $copy->call_number($vol->id);
1785 $circ->target_copy($copy->id);
1789 if( $rec->id == OILS_PRECAT_RECORD ) {
1793 $mvr = $U->record_to_mvr($rec);
1794 $rec->marc(''); # drop the bulky marc data
1808 __PACKAGE__->register_method(
1809 method => "test_batch_circ_events",
1810 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1813 # method for testing the behavior of a given event definition
1814 sub test_batch_circ_events {
1815 my($self, $conn, $auth, $event_def, $barcode) = @_;
1817 my $e = new_editor(authtoken => $auth);
1818 return $e->event unless $e->checkauth;
1819 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1821 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1822 or return $e->event;
1824 my $circ = $e->search_action_circulation(
1825 {target_copy => $copy->id, checkin_time => undef})->[0]
1826 or return $e->event;
1828 return undef unless $circ;
1830 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1834 __PACKAGE__->register_method(
1835 method => "fire_circ_events",
1836 api_name => "open-ils.circ.fire_circ_trigger_events",
1838 General event def runner for circ objects. If no event def ID
1839 is provided, the hook will be used to find the best event_def
1840 match based on the context org unit
1844 __PACKAGE__->register_method(
1845 method => "fire_circ_events",
1846 api_name => "open-ils.circ.fire_hold_trigger_events",
1848 General event def runner for hold objects. If no event def ID
1849 is provided, the hook will be used to find the best event_def
1850 match based on the context org unit
1854 __PACKAGE__->register_method(
1855 method => "fire_circ_events",
1856 api_name => "open-ils.circ.fire_user_trigger_events",
1858 General event def runner for user objects. If no event def ID
1859 is provided, the hook will be used to find the best event_def
1860 match based on the context org unit
1865 sub fire_circ_events {
1866 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1868 my $e = new_editor(authtoken => $auth, xact => 1);
1869 return $e->event unless $e->checkauth;
1873 if($self->api_name =~ /hold/) {
1874 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1875 $targets = $e->batch_retrieve_action_hold_request($target_ids);
1876 } elsif($self->api_name =~ /user/) {
1877 return $e->event unless $e->allowed('VIEW_USER', $org_id);
1878 $targets = $e->batch_retrieve_actor_user($target_ids);
1880 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1881 $targets = $e->batch_retrieve_action_circulation($target_ids);
1883 $e->rollback; # FIXME using transaction because of pgpool + logical replication
1884 # setups where statements in an explicit transaction are sent to
1885 # the primary database in the replica set, but not
1886 # simply making this method authoritative because of weirdness
1887 # with transaction handling in A/T code that causes rollback
1888 # failure down the line if handling many targets
1890 return undef unless @$targets;
1891 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1894 __PACKAGE__->register_method(
1895 method => "user_payments_list",
1896 api_name => "open-ils.circ.user_payments.filtered.batch",
1899 desc => q/Returns a fleshed, date-limited set of all payments a user
1900 has made. By default, ordered by payment date. Optionally
1901 ordered by other columns in the top-level "mp" object/,
1903 {desc => 'Authentication token', type => 'string'},
1904 {desc => 'User ID', type => 'number'},
1905 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
1907 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
1908 and the related fully-realized payment object (e.g money.cash_payment)/}
1912 sub user_payments_list {
1913 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1915 my $e = new_editor(authtoken => $auth);
1916 return $e->event unless $e->checkauth;
1918 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1919 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1921 $order_by ||= ['payment_ts'];
1923 # all payments by user, between start_date and end_date
1924 my $payments = $e->json_query({
1925 select => {mp => ['id']},
1929 fkey => 'xact', field => 'id'}
1933 '+mbt' => {usr => $user_id},
1934 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1936 order_by => {mp => $order_by}
1939 for my $payment_id (@$payments) {
1940 my $payment = $e->retrieve_money_payment([
1948 'credit_card_payment',
1949 'debit_card_payment',
1964 $conn->respond($payment);
1971 __PACKAGE__->register_method(
1972 method => "retrieve_circ_chain",
1973 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
1976 desc => q/Given a circulation, this returns all circulation objects
1977 that are part of the same chain of renewals./,
1979 {desc => 'Authentication token', type => 'string'},
1980 {desc => 'Circ ID', type => 'number'},
1982 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1986 __PACKAGE__->register_method(
1987 method => "retrieve_circ_chain",
1988 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1990 desc => q/Given a circulation, this returns a summary of the circulation objects
1991 that are part of the same chain of renewals./,
1993 {desc => 'Authentication token', type => 'string'},
1994 {desc => 'Circ ID', type => 'number'},
1996 return => {desc => q/Circulation Chain Summary/}
2000 sub retrieve_circ_chain {
2001 my($self, $conn, $auth, $circ_id) = @_;
2003 my $e = new_editor(authtoken => $auth);
2004 return $e->event unless $e->checkauth;
2005 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2007 if($self->api_name =~ /summary/) {
2008 return $U->create_circ_chain_summary($e, $circ_id);
2012 my $chain = $e->json_query({from => ['action.all_circ_chain', $circ_id]});
2014 for my $circ_info (@$chain) {
2015 my $circ = Fieldmapper::action::all_circulation_slim->new;
2016 $circ->$_($circ_info->{$_}) for keys %$circ_info;
2017 $conn->respond($circ);
2024 __PACKAGE__->register_method(
2025 method => "retrieve_prev_circ_chain",
2026 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
2029 desc => q/Given a circulation, this returns all circulation objects
2030 that are part of the previous chain of renewals./,
2032 {desc => 'Authentication token', type => 'string'},
2033 {desc => 'Circ ID', type => 'number'},
2035 return => {desc => q/List of circ objects, orderd by oldest circ first/}
2039 __PACKAGE__->register_method(
2040 method => "retrieve_prev_circ_chain",
2041 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
2043 desc => q/Given a circulation, this returns a summary of the circulation objects
2044 that are part of the previous chain of renewals./,
2046 {desc => 'Authentication token', type => 'string'},
2047 {desc => 'Circ ID', type => 'number'},
2049 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
2053 sub retrieve_prev_circ_chain {
2054 my($self, $conn, $auth, $circ_id) = @_;
2056 my $e = new_editor(authtoken => $auth);
2057 return $e->event unless $e->checkauth;
2058 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2061 $e->json_query({from => ['action.all_circ_chain', $circ_id]})->[0];
2063 my $prev_circ = $e->search_action_all_circulation_slim([
2064 { target_copy => $first_circ->{target_copy},
2065 xact_start => {'<' => $first_circ->{xact_start}}
2074 order_by => { aacs => 'xact_start desc' },
2079 return undef unless $prev_circ;
2081 my $chain_usr = $prev_circ->usr; # note: may be undef
2083 if ($self->api_name =~ /summary/) {
2084 my $sum = $e->json_query({
2086 'action.summarize_all_circ_chain',
2091 my $summary = Fieldmapper::action::circ_chain_summary->new;
2092 $summary->$_($sum->{$_}) for keys %$sum;
2094 return {summary => $summary, usr => $chain_usr};
2098 my $chain = $e->json_query(
2099 {from => ['action.all_circ_chain', $prev_circ->id]});
2101 for my $circ_info (@$chain) {
2102 my $circ = Fieldmapper::action::all_circulation_slim->new;
2103 $circ->$_($circ_info->{$_}) for keys %$circ_info;
2104 $conn->respond($circ);
2111 __PACKAGE__->register_method(
2112 method => "get_copy_due_date",
2113 api_name => "open-ils.circ.copy.due_date.retrieve",
2116 Given a copy ID, returns the due date for the copy if it's
2117 currently circulating. Otherwise, returns null. Note, this is a public
2118 method requiring no authentication. Only the due date is exposed.
2121 {desc => 'Copy ID', type => 'number'}
2123 return => {desc => q/
2124 Due date (ISO date stamp) if the copy is circulating, null otherwise.
2129 sub get_copy_due_date {
2130 my($self, $conn, $copy_id) = @_;
2131 my $e = new_editor();
2133 my $circ = $e->json_query({
2134 select => {circ => ['due_date']},
2137 target_copy => $copy_id,
2138 checkin_time => undef,
2140 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2141 {stop_fines => undef}
2145 })->[0] or return undef;
2147 return $circ->{due_date};
2151 __PACKAGE__->register_method(
2152 method => 'get_offline_data',
2153 api_name => 'open-ils.circ.offline.data.retrieve',
2156 desc => q/Returns a stream of data needed by clients to
2157 support an offline interface. /,
2159 {desc => 'Authentication token', type => 'string'}
2161 return => {desc => q/
2166 sub get_offline_data {
2167 my ($self, $client, $auth) = @_;
2169 my $e = new_editor(authtoken => $auth);
2170 return $e->event unless $e->checkauth;
2171 return $e->event unless $e->allowed('STAFF_LOGIN');
2173 my $org_ids = $U->get_org_ancestors($e->requestor->ws_ou);
2176 idl_class => 'cnct',
2177 data => $e->search_config_non_cataloged_type({owning_lib => $org_ids})
2180 my $surveys = $e->search_action_survey([{
2182 start_date => {'<=' => 'now'},
2183 end_date => {'>=' => 'now'}
2187 asv => ['questions'],
2193 $client->respond({idl_class => 'asv', data => $surveys});
2195 $client->respond({idl_class => 'cit',
2196 data => $e->retrieve_all_config_identification_type});
2198 $client->respond({idl_class => 'cnal',
2199 data => $e->retrieve_all_config_net_access_level});
2201 $client->respond({idl_class => 'fdoc',
2202 data => $e->retrieve_all_config_idl_field_doc});
2204 $client->respond({idl_class => 'pgt',
2205 data => $e->retrieve_all_permission_grp_tree});
2207 my $stat_cats = $U->simplereq(
2209 'open-ils.circ.stat_cat.actor.retrieve.all',
2210 $auth, $e->requestor->ws_ou);
2212 $client->respond({idl_class => 'actsc', data => $stat_cats});
2214 my $settings = $e->search_config_usr_setting_type({
2217 circ.holds_behind_desk
2218 circ.collections.exempt
2221 opac.default_pickup_location
2222 opac.default_sms_carrier
2223 opac.default_sms_notify/]
2227 select => {atevdef => ['opt_in_setting']},
2229 where => {'+atevdef' => {owner => $org_ids}}
2235 $client->respond({idl_class => 'cust', data => $settings});
2242 # {"select":{"acp":["id"],"circ":[{"aggregate":true,"transform":"count","alias":"count","column":"id"}]},"from":{"acp":{"circ":{"field":"target_copy","fkey":"id","type":"left"},"acn"{"field":"id","fkey":"call_number"}}},"where":{"+acn":{"record":200057}}