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_latest_inventory",
375 api_name => "open-ils.circ.circulation.update_latest_inventory");
377 sub update_latest_inventory {
378 my( $self, $conn, $auth, $args ) = @_;
379 my $e = new_editor(authtoken=>$auth, xact=>1);
380 return $e->die_event unless $e->checkauth;
382 my $copies = $$args{copy_list};
383 foreach my $copyid (@$copies) {
384 my $copy = $e->retrieve_asset_copy($copyid);
385 my $alci = $e->search_asset_latest_inventory({copy => $copyid})->[0];
388 $alci->inventory_date('now');
389 $alci->inventory_workstation($e->requestor->wsid);
390 $e->update_asset_latest_inventory($alci) or return $e->die_event;
392 my $alci = Fieldmapper::asset::latest_inventory->new;
393 $alci->inventory_date('now');
394 $alci->inventory_workstation($e->requestor->wsid);
395 $alci->copy($copy->id);
396 $e->create_asset_latest_inventory($alci) or return $e->die_event;
399 $copy->latest_inventory($alci);
405 __PACKAGE__->register_method(
406 method => "set_circ_claims_returned",
407 api_name => "open-ils.circ.circulation.set_claims_returned",
409 desc => q/Sets the circ for a given item as claims returned
410 If a backdate is provided, overdue fines will be voided
411 back to the backdate/,
413 {desc => 'Authentication token', type => 'string'},
414 {desc => 'Arguments, including "barcode" and optional "backdate"', type => 'object'}
416 return => {desc => q/1 on success, failure event on error, and
417 PATRON_EXCEEDS_CLAIMS_RETURN_COUNT if the patron exceeds the
418 configured claims return maximum/}
422 __PACKAGE__->register_method(
423 method => "set_circ_claims_returned",
424 api_name => "open-ils.circ.circulation.set_claims_returned.override",
426 desc => q/This adds support for overrideing the configured max
427 claims returned amount.
428 @see open-ils.circ.circulation.set_claims_returned./,
432 sub set_circ_claims_returned {
433 my( $self, $conn, $auth, $args, $oargs ) = @_;
435 my $e = new_editor(authtoken=>$auth, xact=>1);
436 return $e->die_event unless $e->checkauth;
438 $oargs = { all => 1 } unless defined $oargs;
440 my $barcode = $$args{barcode};
441 my $backdate = $$args{backdate};
443 my $copy = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'})->[0]
444 or return $e->die_event;
446 my $circ = $e->search_action_circulation(
447 {checkin_time => undef, target_copy => $copy->id})->[0]
448 or return $e->die_event;
450 $backdate = $circ->due_date if $$args{use_due_date};
452 $logger->info("marking circ for item $barcode as claims returned".
453 (($backdate) ? " with backdate $backdate" : ''));
455 my $patron = $e->retrieve_actor_user($circ->usr);
456 my $max_count = $U->ou_ancestor_setting_value(
457 $circ->circ_lib, 'circ.max_patron_claim_return_count', $e);
459 # If the patron has too instances of many claims returned,
460 # require an override to continue. A configured max of
461 # 0 means all attempts require an override
462 if(defined $max_count and $patron->claims_returned_count >= $max_count) {
464 if($self->api_name =~ /override/ && ($oargs->{all} || grep { $_ eq 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT' } @{$oargs->{events}})) {
466 # see if we're allowed to override
467 return $e->die_event unless
468 $e->allowed('SET_CIRC_CLAIMS_RETURNED.override', $circ->circ_lib);
472 # exit early and return the max claims return event
474 return OpenILS::Event->new(
475 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT',
477 patron_count => $patron->claims_returned_count,
478 max_count => $max_count
484 $e->allowed('SET_CIRC_CLAIMS_RETURNED', $circ->circ_lib)
485 or return $e->die_event;
487 $circ->stop_fines(OILS_STOP_FINES_CLAIMSRETURNED);
488 $circ->stop_fines_time('now') unless $circ->stop_fines_time;
491 $backdate = clean_ISO8601($backdate);
493 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($circ->due_date));
494 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($backdate);
495 $backdate = $new_date->ymd . 'T' . $original_date->strftime('%T%z');
497 # clean it up once again; need a : in the timezone offset. E.g. -06:00 not -0600
498 $backdate = clean_ISO8601($backdate);
500 # make it look like the circ stopped at the cliams returned time
501 $circ->stop_fines_time($backdate);
502 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate, note => 'System: OVERDUE REVERSED FOR CLAIMS-RETURNED', force_zero => 1});
506 $e->update_action_circulation($circ) or return $e->die_event;
508 # see if there is a configured post-claims-return copy status
509 if(my $stat = $U->ou_ancestor_setting_value($circ->circ_lib, 'circ.claim_return.copy_status')) {
510 $copy->status($stat);
511 $copy->edit_date('now');
512 $copy->editor($e->requestor->id);
513 $e->update_asset_copy($copy) or return $e->die_event;
516 # Check if the copy circ lib wants lost fees voided on claims
518 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_on_claimsreturned', $e))) {
519 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
530 # Check if the copy circ lib wants lost processing fees voided on
532 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_proc_fee_on_claimsreturned', $e))) {
533 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
544 # Check if the copy circ lib wants longoverdue fees voided on claims
546 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_on_claimsreturned', $e))) {
547 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
558 # Check if the copy circ lib wants longoverdue processing fees voided on
560 if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_proc_fee_on_claimsreturned', $e))) {
561 my $result = OpenILS::Application::Circ::CircCommon->void_lost(
572 # Now that all data has been munged, do a no-op update of
573 # the patron to force a change of the last_xact_id value.
574 $e->update_actor_user($e->retrieve_actor_user($circ->usr))
575 or return $e->die_event;
582 __PACKAGE__->register_method(
583 method => "post_checkin_backdate_circ",
584 api_name => "open-ils.circ.post_checkin_backdate",
586 desc => q/Back-date an already checked in circulation/,
588 {desc => 'Authentication token', type => 'string'},
589 {desc => 'Circ ID', type => 'number'},
590 {desc => 'ISO8601 backdate', type => 'string'},
592 return => {desc => q/1 on success, failure event on error/}
596 __PACKAGE__->register_method(
597 method => "post_checkin_backdate_circ",
598 api_name => "open-ils.circ.post_checkin_backdate.batch",
601 desc => q/@see open-ils.circ.post_checkin_backdate. Batch mode/,
603 {desc => 'Authentication token', type => 'string'},
604 {desc => 'List of Circ ID', type => 'array'},
605 {desc => 'ISO8601 backdate', type => 'string'},
607 return => {desc => q/Set of: 1 on success, failure event on error/}
612 sub post_checkin_backdate_circ {
613 my( $self, $conn, $auth, $circ_id, $backdate ) = @_;
614 my $e = new_editor(authtoken=>$auth);
615 return $e->die_event unless $e->checkauth;
616 if($self->api_name =~ /batch/) {
617 foreach my $c (@$circ_id) {
618 $conn->respond(post_checkin_backdate_circ_impl($e, $c, $backdate));
621 $conn->respond_complete(post_checkin_backdate_circ_impl($e, $circ_id, $backdate));
629 sub post_checkin_backdate_circ_impl {
630 my($e, $circ_id, $backdate) = @_;
634 my $circ = $e->retrieve_action_circulation($circ_id)
635 or return $e->die_event;
637 # anyone with checkin perms can backdate (more restrictive?)
638 return $e->die_event unless $e->allowed('COPY_CHECKIN', $circ->circ_lib);
640 # don't allow back-dating an open circulation
641 return OpenILS::Event->new('BAD_PARAMS') unless
642 $backdate and $circ->checkin_time;
644 # update the checkin and stop_fines times to reflect the new backdate
645 $circ->stop_fines_time(clean_ISO8601($backdate));
646 $circ->checkin_time(clean_ISO8601($backdate));
647 $e->update_action_circulation($circ) or return $e->die_event;
649 # now void the overdues "erased" by the back-dating
650 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate});
653 # If the circ was closed before and the balance owned !=0, re-open the transaction
654 $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
663 __PACKAGE__->register_method (
664 method => 'set_circ_due_date',
665 api_name => 'open-ils.circ.circulation.due_date.update',
667 Updates the due_date on the given circ
669 @param circid The id of the circ to update
670 @param date The timestamp of the new due date
674 sub set_circ_due_date {
675 my( $self, $conn, $auth, $circ_id, $date ) = @_;
677 my $e = new_editor(xact=>1, authtoken=>$auth);
678 return $e->die_event unless $e->checkauth;
679 my $circ = $e->retrieve_action_circulation($circ_id)
680 or return $e->die_event;
682 return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
683 $date = clean_ISO8601($date);
685 if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
686 my $original_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($circ->due_date));
687 my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
689 # since the new date may be coming in as UTC, convert it
690 # to the same time zone as the original due date so that
691 # ->ymd is more likely to yield the expected results
692 $new_date->set_time_zone($original_date->time_zone());
693 $date = clean_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
696 $circ->due_date($date);
697 $e->update_action_circulation($circ) or return $e->die_event;
704 __PACKAGE__->register_method(
705 method => "create_in_house_use",
706 api_name => 'open-ils.circ.in_house_use.create',
708 Creates an in-house use action.
709 @param $authtoken The login session key
710 @param params A hash of params including
711 'location' The org unit id where the in-house use occurs
712 'copyid' The copy in question
713 'count' The number of in-house uses to apply to this copy
714 @return An array of id's representing the id's of the newly created
715 in-house use objects or an event on an error
718 __PACKAGE__->register_method(
719 method => "create_in_house_use",
720 api_name => 'open-ils.circ.non_cat_in_house_use.create',
724 sub create_in_house_use {
725 my( $self, $client, $auth, $params ) = @_;
728 my $org = $params->{location};
729 my $copyid = $params->{copyid};
730 my $count = $params->{count} || 1;
731 my $nc_type = $params->{non_cat_type};
732 my $use_time = $params->{use_time} || 'now';
734 my $e = new_editor(xact=>1,authtoken=>$auth);
735 return $e->event unless $e->checkauth;
736 return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
738 my $non_cat = 1 if $self->api_name =~ /non_cat/;
742 $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
744 $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
750 if( $use_time ne 'now' ) {
751 $use_time = clean_ISO8601($use_time);
752 $logger->debug("in_house_use setting use time to $use_time");
763 $ihu = Fieldmapper::action::non_cat_in_house_use->new;
764 $ihu->item_type($nc_type);
765 $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
766 $cmeth = "create_action_non_cat_in_house_use";
769 $ihu = Fieldmapper::action::in_house_use->new;
771 $method = 'open-ils.storage.direct.action.in_house_use.create';
772 $cmeth = "create_action_in_house_use";
775 $ihu->staff($e->requestor->id);
776 $ihu->org_unit($org);
777 $ihu->use_time($use_time);
779 $ihu = $e->$cmeth($ihu) or return $e->event;
780 push( @ids, $ihu->id );
791 __PACKAGE__->register_method(
792 method => "view_circs",
793 api_name => "open-ils.circ.copy_checkout_history.retrieve",
795 Retrieves the last X circs for a given copy
796 @param authtoken The login session key
797 @param copyid The copy to check
798 @param count How far to go back in the item history
799 @return An array of circ ids
802 # ----------------------------------------------------------------------
803 # Returns $count most recent circs. If count exceeds the configured
804 # max, use the configured max instead
805 # ----------------------------------------------------------------------
807 my( $self, $client, $authtoken, $copyid, $count ) = @_;
809 my $e = new_editor(authtoken => $authtoken);
810 return $e->event unless $e->checkauth;
812 my $copy = $e->retrieve_asset_copy([
815 flesh_fields => {acp => ['call_number']}
817 ]) or return $e->event;
819 return $e->event unless $e->allowed(
820 'VIEW_COPY_CHECKOUT_HISTORY',
821 ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ?
822 $copy->circ_lib : $copy->call_number->owning_lib);
824 my $max_history = $U->ou_ancestor_setting_value(
825 $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
827 if(defined $max_history) {
828 $count = $max_history unless defined $count and $count < $max_history;
830 $count = 4 unless defined $count;
833 return $e->search_action_all_circulation_slim([
834 {target_copy => $copyid},
835 {limit => $count, order_by => { aacs => "xact_start DESC" }}
840 __PACKAGE__->register_method(
841 method => "circ_count",
842 api_name => "open-ils.circ.circulation.count",
844 Returns the number of times the item has circulated
845 @param copyid The copy to check
849 my( $self, $client, $copyid ) = @_;
851 my $count = new_editor()->json_query({
860 where => {'+circbyyr' => {copy => $copyid}}
872 __PACKAGE__->register_method(
873 method => 'fetch_notes',
875 api_name => 'open-ils.circ.copy_note.retrieve.all',
877 Returns an array of copy note objects.
878 @param args A named hash of parameters including:
879 authtoken : Required if viewing non-public notes
880 itemid : The id of the item whose notes we want to retrieve
881 pub : True if all the caller wants are public notes
882 @return An array of note objects
885 __PACKAGE__->register_method(
886 method => 'fetch_notes',
887 api_name => 'open-ils.circ.call_number_note.retrieve.all',
888 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
890 __PACKAGE__->register_method(
891 method => 'fetch_notes',
892 api_name => 'open-ils.circ.title_note.retrieve.all',
893 signature => q/@see open-ils.circ.copy_note.retrieve.all/);
896 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
898 my( $self, $connection, $args ) = @_;
900 my $id = $$args{itemid};
901 my $authtoken = $$args{authtoken};
904 if( $self->api_name =~ /copy/ ) {
906 return $U->cstorereq(
907 'open-ils.cstore.direct.asset.copy_note.search.atomic',
908 { owning_copy => $id, pub => 't' } );
910 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
912 return $U->cstorereq(
913 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
916 } elsif( $self->api_name =~ /call_number/ ) {
918 return $U->cstorereq(
919 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
920 { call_number => $id, pub => 't' } );
922 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
924 return $U->cstorereq(
925 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
928 } elsif( $self->api_name =~ /title/ ) {
930 return $U->cstorereq(
931 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
932 { record => $id, pub => 't' } );
934 ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
936 return $U->cstorereq(
937 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
944 __PACKAGE__->register_method(
945 method => 'has_notes',
946 api_name => 'open-ils.circ.copy.has_notes');
947 __PACKAGE__->register_method(
948 method => 'has_notes',
949 api_name => 'open-ils.circ.call_number.has_notes');
950 __PACKAGE__->register_method(
951 method => 'has_notes',
952 api_name => 'open-ils.circ.title.has_notes');
956 my( $self, $conn, $authtoken, $id ) = @_;
957 my $editor = new_editor(authtoken => $authtoken);
958 return $editor->event unless $editor->checkauth;
960 my $n = $editor->search_asset_copy_note(
961 {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
963 $n = $editor->search_asset_call_number_note(
964 {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
966 $n = $editor->search_biblio_record_note(
967 {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
974 __PACKAGE__->register_method(
975 method => 'create_copy_note',
976 api_name => 'open-ils.circ.copy_note.create',
978 Creates a new copy note
979 @param authtoken The login session key
980 @param note The note object to create
981 @return The id of the new note object
984 sub create_copy_note {
985 my( $self, $connection, $authtoken, $note ) = @_;
987 my $e = new_editor(xact=>1, authtoken=>$authtoken);
988 return $e->event unless $e->checkauth;
989 my $copy = $e->retrieve_asset_copy(
993 flesh_fields => { 'acp' => ['call_number'] }
998 return $e->event unless
999 $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
1001 $note->create_date('now');
1002 $note->creator($e->requestor->id);
1003 $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
1006 $e->create_asset_copy_note($note) or return $e->event;
1012 __PACKAGE__->register_method(
1013 method => 'delete_copy_note',
1014 api_name => 'open-ils.circ.copy_note.delete',
1016 Deletes an existing copy note
1017 @param authtoken The login session key
1018 @param noteid The id of the note to delete
1019 @return 1 on success - Event otherwise.
1021 sub delete_copy_note {
1022 my( $self, $conn, $authtoken, $noteid ) = @_;
1024 my $e = new_editor(xact=>1, authtoken=>$authtoken);
1025 return $e->die_event unless $e->checkauth;
1027 my $note = $e->retrieve_asset_copy_note([
1031 'acpn' => [ 'owning_copy' ],
1032 'acp' => [ 'call_number' ],
1035 ]) or return $e->die_event;
1037 if( $note->creator ne $e->requestor->id ) {
1038 return $e->die_event unless
1039 $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
1042 $e->delete_asset_copy_note($note) or return $e->die_event;
1047 __PACKAGE__->register_method(
1048 method => 'fetch_copy_tags',
1050 api_name => 'open-ils.circ.copy_tags.retrieve',
1052 Returns an array of publicly-visible copy tag objects.
1053 @param args A named hash of parameters including:
1054 copy_id : The id of the item whose notes we want to retrieve
1055 tag_type : Type of copy tags to retrieve, e.g., 'bookplate' (optional)
1056 scope : top of org subtree whose copy tags we want to see
1057 depth : how far down to look for copy tags (optional)
1058 @return An array of copy tag objects
1060 __PACKAGE__->register_method(
1061 method => 'fetch_copy_tags',
1063 api_name => 'open-ils.circ.copy_tags.retrieve.staff',
1065 Returns an array of all copy tag objects.
1066 @param args A named hash of parameters including:
1067 authtoken : Required to view non-public notes
1068 copy_id : The id of the item whose notes we want to retrieve (optional)
1069 tag_type : Type of copy tags to retrieve, e.g., 'bookplate'
1070 scope : top of org subtree whose copy tags we want to see
1071 depth : how far down to look for copy tags (optional)
1072 @return An array of copy tag objects
1075 sub fetch_copy_tags {
1076 my ($self, $conn, $args) = @_;
1078 my $org = $args->{scope};
1079 my $depth = $args->{depth};
1083 if ($self->api_name =~ /\.staff/) {
1084 my $authtoken = $args->{authtoken};
1085 return new OpenILS::Event("BAD_PARAMS", "desc" => "authtoken required") unless defined $authtoken;
1086 $e = new_editor(authtoken => $args->{authtoken});
1087 return $e->event unless $e->checkauth;
1088 return $e->event unless $e->allowed('STAFF_LOGIN', $org);
1091 $filter->{pub} = 't';
1093 $filter->{tag_type} = $args->{tag_type} if exists($args->{tag_type});
1094 $filter->{'+acptcm'} = {
1095 copy => $args->{copy_id}
1098 # filter by owner of copy tag and depth
1099 $filter->{owner} = {
1101 select => {aou => [{
1103 transform => 'actor.org_unit_descendants',
1104 result_field => 'id',
1105 (defined($depth) ? ( params => [$depth] ) : ()),
1108 where => {id => $org}
1112 return $e->search_asset_copy_tag([$filter, {
1113 join => { acptcm => {} },
1115 flesh_fields => { acpt => ['tag_type'] }
1120 __PACKAGE__->register_method(
1121 method => 'age_hold_rules',
1122 api_name => 'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
1125 sub age_hold_rules {
1126 my( $self, $conn ) = @_;
1127 return new_editor()->retrieve_all_config_rules_age_hold_protect();
1132 __PACKAGE__->register_method(
1133 method => 'copy_details_barcode',
1135 api_name => 'open-ils.circ.copy_details.retrieve.barcode');
1136 sub copy_details_barcode {
1137 my( $self, $conn, $auth, $barcode ) = @_;
1138 my $e = new_editor();
1139 my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
1140 return $e->event unless $cid;
1141 return copy_details( $self, $conn, $auth, $cid );
1145 __PACKAGE__->register_method(
1146 method => 'copy_details',
1147 api_name => 'open-ils.circ.copy_details.retrieve');
1150 my( $self, $conn, $auth, $copy_id ) = @_;
1151 my $e = new_editor(authtoken=>$auth);
1152 return $e->event unless $e->checkauth;
1154 my $flesh = { flesh => 1 };
1156 my $copy = $e->retrieve_asset_copy(
1162 acp => ['call_number','parts','peer_record_maps','floating'],
1163 acn => ['record','prefix','suffix','label_class']
1166 ]) or return $e->event;
1169 # De-flesh the copy for backwards compatibility
1171 my $vol = $copy->call_number;
1173 $copy->call_number($vol->id);
1174 my $record = $vol->record;
1176 $vol->record($record->id);
1177 $mvr = $U->record_to_mvr($record);
1182 my $hold = $e->search_action_hold_request(
1184 current_copy => $copy_id,
1185 capture_time => { "!=" => undef },
1186 fulfillment_time => undef,
1187 cancel_time => undef,
1191 OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1193 my $transit = $e->search_action_transit_copy(
1194 { target_copy => $copy_id, dest_recv_time => undef, cancel_time => undef } )->[0];
1196 # find the most recent circulation for the requested copy,
1197 # be it active, completed, or aged.
1198 my $circ = $e->search_action_all_circulation_slim([
1199 { target_copy => $copy_id },
1205 'checkin_workstation',
1208 'recurring_fine_rule'
1211 order_by => { aacs => 'xact_start desc' },
1219 transit => $transit,
1229 __PACKAGE__->register_method(
1230 method => 'mark_item',
1231 api_name => 'open-ils.circ.mark_item_damaged',
1233 Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1234 @param authtoken The login session key
1235 @param copy_id The ID of the copy to mark as damaged
1236 @return 1 on success - Event otherwise.
1239 __PACKAGE__->register_method(
1240 method => 'mark_item',
1241 api_name => 'open-ils.circ.mark_item_missing',
1243 Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1244 @param authtoken The login session key
1245 @param copy_id The ID of the copy to mark as missing
1246 @return 1 on success - Event otherwise.
1249 __PACKAGE__->register_method(
1250 method => 'mark_item',
1251 api_name => 'open-ils.circ.mark_item_bindery',
1253 Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1254 @param authtoken The login session key
1255 @param copy_id The ID of the copy to mark as bindery
1256 @return 1 on success - Event otherwise.
1259 __PACKAGE__->register_method(
1260 method => 'mark_item',
1261 api_name => 'open-ils.circ.mark_item_on_order',
1263 Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1264 @param authtoken The login session key
1265 @param copy_id The ID of the copy to mark as on order
1266 @return 1 on success - Event otherwise.
1269 __PACKAGE__->register_method(
1270 method => 'mark_item',
1271 api_name => 'open-ils.circ.mark_item_ill',
1273 Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1274 @param authtoken The login session key
1275 @param copy_id The ID of the copy to mark as inter-library loan
1276 @return 1 on success - Event otherwise.
1279 __PACKAGE__->register_method(
1280 method => 'mark_item',
1281 api_name => 'open-ils.circ.mark_item_cataloging',
1283 Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1284 @param authtoken The login session key
1285 @param copy_id The ID of the copy to mark as cataloging
1286 @return 1 on success - Event otherwise.
1289 __PACKAGE__->register_method(
1290 method => 'mark_item',
1291 api_name => 'open-ils.circ.mark_item_reserves',
1293 Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1294 @param authtoken The login session key
1295 @param copy_id The ID of the copy to mark as reserves
1296 @return 1 on success - Event otherwise.
1299 __PACKAGE__->register_method(
1300 method => 'mark_item',
1301 api_name => 'open-ils.circ.mark_item_discard',
1303 Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1304 @param authtoken The login session key
1305 @param copy_id The ID of the copy to mark as discard
1306 @return 1 on success - Event otherwise.
1311 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1314 my $e = new_editor(authtoken=>$auth);
1315 return $e->die_event unless $e->checkauth;
1316 my $copy = $e->retrieve_asset_copy([
1318 {flesh => 1, flesh_fields => {'acp' => ['call_number','status']}}])
1319 or return $e->die_event;
1322 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1323 $copy->circ_lib : $copy->call_number->owning_lib;
1325 my $evt; # For later.
1326 my $perm = 'MARK_ITEM_MISSING';
1327 my $stat = OILS_COPY_STATUS_MISSING;
1329 if( $self->api_name =~ /damaged/ ) {
1330 $perm = 'MARK_ITEM_DAMAGED';
1331 $stat = OILS_COPY_STATUS_DAMAGED;
1332 } elsif ( $self->api_name =~ /bindery/ ) {
1333 $perm = 'MARK_ITEM_BINDERY';
1334 $stat = OILS_COPY_STATUS_BINDERY;
1335 } elsif ( $self->api_name =~ /on_order/ ) {
1336 $perm = 'MARK_ITEM_ON_ORDER';
1337 $stat = OILS_COPY_STATUS_ON_ORDER;
1338 } elsif ( $self->api_name =~ /ill/ ) {
1339 $perm = 'MARK_ITEM_ILL';
1340 $stat = OILS_COPY_STATUS_ILL;
1341 } elsif ( $self->api_name =~ /cataloging/ ) {
1342 $perm = 'MARK_ITEM_CATALOGING';
1343 $stat = OILS_COPY_STATUS_CATALOGING;
1344 } elsif ( $self->api_name =~ /reserves/ ) {
1345 $perm = 'MARK_ITEM_RESERVES';
1346 $stat = OILS_COPY_STATUS_RESERVES;
1347 } elsif ( $self->api_name =~ /discard/ ) {
1348 $perm = 'MARK_ITEM_DISCARD';
1349 $stat = OILS_COPY_STATUS_DISCARD;
1352 # caller may proceed if either perm is allowed
1353 return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1355 # Copy status checks.
1356 if ($copy->status->id() == OILS_COPY_STATUS_CHECKED_OUT) {
1357 # Items must be checked in before any attempt is made to change its status.
1358 if ($args->{handle_checkin}) {
1359 $evt = try_checkin($auth, $copy_id);
1361 $evt = OpenILS::Event->new('ITEM_TO_MARK_CHECKED_OUT');
1363 } elsif ($copy->status->id() == OILS_COPY_STATUS_IN_TRANSIT) {
1364 # Items in transit need to have the transit aborted before being marked.
1365 if ($args->{handle_transit}) {
1366 $evt = try_abort_transit($auth, $copy_id);
1368 $evt = OpenILS::Event->new('ITEM_TO_MARK_IN_TRANSIT');
1370 } elsif ($U->is_true($copy->status->restrict_copy_delete()) && $self->api_name =~ /discard/) {
1371 # Items with restrict_copy_delete status require the
1372 # COPY_DELETE_WARNING.override permission to be marked for
1374 if ($args->{handle_copy_delete_warning}) {
1375 $evt = $e->event unless $e->allowed(['COPY_DELETE_WARNING.override'], $owning_lib);
1377 $evt = OpenILS::Event->new('COPY_DELETE_WARNING');
1380 return $evt if $evt;
1382 # Retrieving holds for later use.
1383 my $holds = $e->search_action_hold_request([
1385 current_copy => $copy->id,
1386 fulfillment_time => undef,
1387 cancel_time => undef,
1389 {flesh=>1, flesh_fields=>{ahr=>['eligible_copies']}}
1392 # Throw event if attempting to mark discard the only copy to fill a hold.
1393 if ($self->api_name =~ /discard/) {
1394 if (!$args->{handle_last_hold_copy}) {
1395 for my $hold (@$holds) {
1396 my $eligible = $hold->eligible_copies();
1397 if (!defined($eligible) || scalar(@{$eligible}) < 2) {
1398 $evt = OpenILS::Event->new('ITEM_TO_MARK_LAST_HOLD_COPY');
1404 return $evt if $evt;
1406 # Things below here require a transaction and there is nothing left to interfere with it.
1409 # Handle extra mark damaged charges, etc.
1410 if ($self->api_name =~ /damaged/) {
1411 $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1412 return $evt if $evt;
1416 $copy->status($stat);
1417 $copy->edit_date('now');
1418 $copy->editor($e->requestor->id);
1420 $e->update_asset_copy($copy) or return $e->die_event;
1424 if( $self->api_name =~ /damaged/ ) {
1425 # now that we've committed the changes, create related A/T events
1426 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1427 $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1430 $logger->debug("resetting holds that target the marked copy");
1431 OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1437 my($auth, $copy_id) = @_;
1439 my $checkin = $U->simplereq(
1441 'open-ils.circ.checkin.override',
1443 copy_id => $copy_id,
1447 if(ref $checkin ne 'ARRAY') { $checkin = [$checkin]; }
1449 my $evt_code = $checkin->[0]->{textcode};
1450 $logger->info("try_checkin() received event: $evt_code");
1452 if($evt_code eq 'SUCCESS' || $evt_code eq 'NO_CHANGE') {
1453 $logger->info('try_checkin() successful checkin');
1456 $logger->warn('try_checkin() un-successful checkin');
1461 sub try_abort_transit {
1462 my ($auth, $copy_id) = @_;
1464 my $abort = $U->simplereq(
1466 'open-ils.circ.transit.abort',
1467 $auth, {copyid => $copy_id}
1469 # Above returns 1 or an event.
1470 return $abort if (ref $abort);
1474 sub handle_mark_damaged {
1475 my($e, $copy, $owning_lib, $args) = @_;
1477 my $apply = $args->{apply_fines} || '';
1478 return undef if $apply eq 'noapply';
1480 my $new_amount = $args->{override_amount};
1481 my $new_btype = $args->{override_btype};
1482 my $new_note = $args->{override_note};
1484 # grab the last circulation
1485 my $circ = $e->search_action_circulation([
1486 { target_copy => $copy->id},
1488 order_by => {circ => "xact_start DESC"},
1490 flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1494 return undef unless $circ;
1496 my $charge_price = $U->ou_ancestor_setting_value(
1497 $owning_lib, 'circ.charge_on_damaged', $e);
1499 my $proc_fee = $U->ou_ancestor_setting_value(
1500 $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1502 my $void_overdue = $U->ou_ancestor_setting_value(
1503 $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1505 return undef unless $charge_price or $proc_fee;
1507 my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1508 my $total = $copy_price + $proc_fee;
1512 if($new_amount and $new_btype) {
1514 # Allow staff to override the amount to charge for a damaged item
1515 # Consider the case where the item is only partially damaged
1516 # This value is meant to take the place of the item price and
1517 # optional processing fee.
1519 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1520 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1521 return $evt if $evt;
1525 if($charge_price and $copy_price) {
1526 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1527 $e, $copy_price, 7, 'Damaged Item', $circ->id);
1528 return $evt if $evt;
1532 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1533 $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1534 return $evt if $evt;
1538 # the assumption is that you would not void the overdues unless you
1539 # were also charging for the item and/or applying a processing fee
1541 my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {note => 'System: OVERDUE REVERSED FOR DAMAGE CHARGE'});
1542 return $evt if $evt;
1545 my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1546 return $evt if $evt;
1548 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1549 $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1551 my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1552 return $evt2 if $evt2;
1555 return OpenILS::Event->new('DAMAGE_CHARGE',
1566 # ----------------------------------------------------------------------
1567 __PACKAGE__->register_method(
1568 method => 'mark_item_missing_pieces',
1569 api_name => 'open-ils.circ.mark_item_missing_pieces',
1571 Changes the status of a copy to "damaged" or to a custom status based on the
1572 circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1574 @param authtoken The login session key
1575 @param copy_id The ID of the copy to mark as damaged
1576 @return Success event with circ and copy objects in the payload, or error Event otherwise.
1580 sub mark_item_missing_pieces {
1581 my( $self, $conn, $auth, $copy_id, $args ) = @_;
1582 ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1583 ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1585 my $e2 = new_editor(authtoken=>$auth, xact =>1);
1586 return $e2->die_event unless $e2->checkauth;
1589 my $copy = $e2->retrieve_asset_copy([
1591 {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1592 or return $e2->die_event;
1595 ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ?
1596 $copy->circ_lib : $copy->call_number->owning_lib;
1598 return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1600 #### grab the last circulation
1601 my $circ = $e2->search_action_circulation([
1602 { target_copy => $copy->id},
1604 order_by => {circ => "xact_start DESC"}
1609 $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1611 return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1614 my $holds = $e2->search_action_hold_request(
1616 current_copy => $copy->id,
1617 fulfillment_time => undef,
1618 cancel_time => undef,
1622 $logger->debug("resetting holds that target the marked copy");
1623 OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1626 if (! $e2->commit) {
1627 return $e2->die_event;
1630 my $e = new_editor(authtoken=>$auth, xact =>1);
1631 return $e->die_event unless $e->checkauth;
1633 if (! $circ->checkin_time) { # if circ active, attempt renew
1634 my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1635 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1636 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1637 $circ = $res->[0]->{payload}{'circ'};
1638 $circ->target_copy( $copy->id );
1639 $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1641 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1646 'copy_id'=>$circ->target_copy,
1647 'patron_id'=>$circ->usr,
1648 'skip_deposit_fee'=>1,
1649 'skip_rental_fee'=>1
1652 if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1654 my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1655 $e, $copy, $e->requestor, 1 );
1657 if ($hold) { # needed for hold? then due now
1659 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1660 my $due_date = DateTime->now(time_zone => 'local');
1661 $co_params->{'due_date'} = clean_ISO8601( $due_date->strftime('%FT%T%z') );
1663 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1667 my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1668 if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1669 if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1670 $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1671 $circ = $res->[0]->{payload}{'circ'};
1673 $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1679 ### Update the item status
1681 my $custom_stat = $U->ou_ancestor_setting_value(
1682 $owning_lib, 'circ.missing_pieces.copy_status', $e);
1683 my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1685 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1686 $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1688 $copy->status($stat);
1689 $copy->edit_date('now');
1690 $copy->editor($e->requestor->id);
1692 $e->update_asset_copy($copy) or return $e->die_event;
1696 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1697 $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1699 return OpenILS::Event->new('SUCCESS',
1703 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1704 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1709 return $e->die_event;
1717 # ----------------------------------------------------------------------
1718 __PACKAGE__->register_method(
1719 method => 'magic_fetch',
1720 api_name => 'open-ils.agent.fetch'
1723 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1726 my( $self, $conn, $auth, $args ) = @_;
1727 my $e = new_editor( authtoken => $auth );
1728 return $e->event unless $e->checkauth;
1730 my $hint = $$args{hint};
1731 my $id = $$args{id};
1733 # Is the call allowed to fetch this type of object?
1734 return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1736 # Find the class the implements the given hint
1737 my ($class) = grep {
1738 $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1740 $class =~ s/Fieldmapper:://og;
1741 $class =~ s/::/_/og;
1742 my $method = "retrieve_$class";
1744 my $obj = $e->$method($id) or return $e->event;
1747 # ----------------------------------------------------------------------
1750 __PACKAGE__->register_method(
1751 method => "fleshed_circ_retrieve",
1753 api_name => "open-ils.circ.fleshed.retrieve",);
1755 sub fleshed_circ_retrieve {
1756 my( $self, $client, $id ) = @_;
1757 my $e = new_editor();
1758 my $circ = $e->retrieve_action_circulation(
1764 circ => [ qw/ target_copy / ],
1765 acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1766 ascecm => [ qw/ stat_cat stat_cat_entry / ],
1767 acn => [ qw/ record / ],
1771 ) or return $e->event;
1773 my $copy = $circ->target_copy;
1774 my $vol = $copy->call_number;
1775 my $rec = $circ->target_copy->call_number->record;
1777 $vol->record($rec->id);
1778 $copy->call_number($vol->id);
1779 $circ->target_copy($copy->id);
1783 if( $rec->id == OILS_PRECAT_RECORD ) {
1787 $mvr = $U->record_to_mvr($rec);
1788 $rec->marc(''); # drop the bulky marc data
1802 __PACKAGE__->register_method(
1803 method => "test_batch_circ_events",
1804 api_name => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1807 # method for testing the behavior of a given event definition
1808 sub test_batch_circ_events {
1809 my($self, $conn, $auth, $event_def, $barcode) = @_;
1811 my $e = new_editor(authtoken => $auth);
1812 return $e->event unless $e->checkauth;
1813 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1815 my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1816 or return $e->event;
1818 my $circ = $e->search_action_circulation(
1819 {target_copy => $copy->id, checkin_time => undef})->[0]
1820 or return $e->event;
1822 return undef unless $circ;
1824 return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1828 __PACKAGE__->register_method(
1829 method => "fire_circ_events",
1830 api_name => "open-ils.circ.fire_circ_trigger_events",
1832 General event def runner for circ objects. If no event def ID
1833 is provided, the hook will be used to find the best event_def
1834 match based on the context org unit
1838 __PACKAGE__->register_method(
1839 method => "fire_circ_events",
1840 api_name => "open-ils.circ.fire_hold_trigger_events",
1842 General event def runner for hold objects. If no event def ID
1843 is provided, the hook will be used to find the best event_def
1844 match based on the context org unit
1848 __PACKAGE__->register_method(
1849 method => "fire_circ_events",
1850 api_name => "open-ils.circ.fire_user_trigger_events",
1852 General event def runner for user objects. If no event def ID
1853 is provided, the hook will be used to find the best event_def
1854 match based on the context org unit
1859 sub fire_circ_events {
1860 my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1862 my $e = new_editor(authtoken => $auth, xact => 1);
1863 return $e->event unless $e->checkauth;
1867 if($self->api_name =~ /hold/) {
1868 return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1869 $targets = $e->batch_retrieve_action_hold_request($target_ids);
1870 } elsif($self->api_name =~ /user/) {
1871 return $e->event unless $e->allowed('VIEW_USER', $org_id);
1872 $targets = $e->batch_retrieve_actor_user($target_ids);
1874 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1875 $targets = $e->batch_retrieve_action_circulation($target_ids);
1877 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1878 # simply making this method authoritative because of weirdness
1879 # with transaction handling in A/T code that causes rollback
1880 # failure down the line if handling many targets
1882 return undef unless @$targets;
1883 return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1886 __PACKAGE__->register_method(
1887 method => "user_payments_list",
1888 api_name => "open-ils.circ.user_payments.filtered.batch",
1891 desc => q/Returns a fleshed, date-limited set of all payments a user
1892 has made. By default, ordered by payment date. Optionally
1893 ordered by other columns in the top-level "mp" object/,
1895 {desc => 'Authentication token', type => 'string'},
1896 {desc => 'User ID', type => 'number'},
1897 {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'}
1899 return => {desc => q/List of "mp" objects, fleshed with the billable transaction
1900 and the related fully-realized payment object (e.g money.cash_payment)/}
1904 sub user_payments_list {
1905 my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1907 my $e = new_editor(authtoken => $auth);
1908 return $e->event unless $e->checkauth;
1910 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1911 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1913 $order_by ||= ['payment_ts'];
1915 # all payments by user, between start_date and end_date
1916 my $payments = $e->json_query({
1917 select => {mp => ['id']},
1921 fkey => 'xact', field => 'id'}
1925 '+mbt' => {usr => $user_id},
1926 '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1928 order_by => {mp => $order_by}
1931 for my $payment_id (@$payments) {
1932 my $payment = $e->retrieve_money_payment([
1940 'credit_card_payment',
1941 'debit_card_payment',
1956 $conn->respond($payment);
1963 __PACKAGE__->register_method(
1964 method => "retrieve_circ_chain",
1965 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ",
1968 desc => q/Given a circulation, this returns all circulation objects
1969 that are part of the same chain of renewals./,
1971 {desc => 'Authentication token', type => 'string'},
1972 {desc => 'Circ ID', type => 'number'},
1974 return => {desc => q/List of circ objects, orderd by oldest circ first/}
1978 __PACKAGE__->register_method(
1979 method => "retrieve_circ_chain",
1980 api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1982 desc => q/Given a circulation, this returns a summary of the circulation objects
1983 that are part of the same chain of renewals./,
1985 {desc => 'Authentication token', type => 'string'},
1986 {desc => 'Circ ID', type => 'number'},
1988 return => {desc => q/Circulation Chain Summary/}
1992 sub retrieve_circ_chain {
1993 my($self, $conn, $auth, $circ_id) = @_;
1995 my $e = new_editor(authtoken => $auth);
1996 return $e->event unless $e->checkauth;
1997 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1999 if($self->api_name =~ /summary/) {
2000 return $U->create_circ_chain_summary($e, $circ_id);
2004 my $chain = $e->json_query({from => ['action.all_circ_chain', $circ_id]});
2006 for my $circ_info (@$chain) {
2007 my $circ = Fieldmapper::action::all_circulation_slim->new;
2008 $circ->$_($circ_info->{$_}) for keys %$circ_info;
2009 $conn->respond($circ);
2016 __PACKAGE__->register_method(
2017 method => "retrieve_prev_circ_chain",
2018 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
2021 desc => q/Given a circulation, this returns all circulation objects
2022 that are part of the previous chain of renewals./,
2024 {desc => 'Authentication token', type => 'string'},
2025 {desc => 'Circ ID', type => 'number'},
2027 return => {desc => q/List of circ objects, orderd by oldest circ first/}
2031 __PACKAGE__->register_method(
2032 method => "retrieve_prev_circ_chain",
2033 api_name => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
2035 desc => q/Given a circulation, this returns a summary of the circulation objects
2036 that are part of the previous chain of renewals./,
2038 {desc => 'Authentication token', type => 'string'},
2039 {desc => 'Circ ID', type => 'number'},
2041 return => {desc => q/Object containing Circulation Chain Summary and User Id/}
2045 sub retrieve_prev_circ_chain {
2046 my($self, $conn, $auth, $circ_id) = @_;
2048 my $e = new_editor(authtoken => $auth);
2049 return $e->event unless $e->checkauth;
2050 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2053 $e->json_query({from => ['action.all_circ_chain', $circ_id]})->[0];
2055 my $prev_circ = $e->search_action_all_circulation_slim([
2056 { target_copy => $first_circ->{target_copy},
2057 xact_start => {'<' => $first_circ->{xact_start}}
2066 order_by => { aacs => 'xact_start desc' },
2071 return undef unless $prev_circ;
2073 my $chain_usr = $prev_circ->usr; # note: may be undef
2075 if ($self->api_name =~ /summary/) {
2076 my $sum = $e->json_query({
2078 'action.summarize_all_circ_chain',
2083 my $summary = Fieldmapper::action::circ_chain_summary->new;
2084 $summary->$_($sum->{$_}) for keys %$sum;
2086 return {summary => $summary, usr => $chain_usr};
2090 my $chain = $e->json_query(
2091 {from => ['action.all_circ_chain', $prev_circ->id]});
2093 for my $circ_info (@$chain) {
2094 my $circ = Fieldmapper::action::all_circulation_slim->new;
2095 $circ->$_($circ_info->{$_}) for keys %$circ_info;
2096 $conn->respond($circ);
2103 __PACKAGE__->register_method(
2104 method => "get_copy_due_date",
2105 api_name => "open-ils.circ.copy.due_date.retrieve",
2108 Given a copy ID, returns the due date for the copy if it's
2109 currently circulating. Otherwise, returns null. Note, this is a public
2110 method requiring no authentication. Only the due date is exposed.
2113 {desc => 'Copy ID', type => 'number'}
2115 return => {desc => q/
2116 Due date (ISO date stamp) if the copy is circulating, null otherwise.
2121 sub get_copy_due_date {
2122 my($self, $conn, $copy_id) = @_;
2123 my $e = new_editor();
2125 my $circ = $e->json_query({
2126 select => {circ => ['due_date']},
2129 target_copy => $copy_id,
2130 checkin_time => undef,
2132 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2133 {stop_fines => undef}
2137 })->[0] or return undef;
2139 return $circ->{due_date};
2146 # {"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}}